diff --git a/examples/xc-transactor/esbuild.dev.mjs b/examples/xc-transactor/esbuild.dev.mjs new file mode 100644 index 00000000..8b010031 --- /dev/null +++ b/examples/xc-transactor/esbuild.dev.mjs @@ -0,0 +1,24 @@ +import esbuild from 'esbuild'; +import { wasmLoader } from 'esbuild-plugin-wasm'; +import { createProxyServer } from '../../esbuild.proxy.mjs'; + +const plugins = [wasmLoader({ mode: 'deferred' })]; + +const options = { + entryPoints: ['src/index.ts'], + bundle: true, + format: 'esm', + platform: 'browser', + target: 'esnext', + preserveSymlinks: true, + treeShaking: true, + sourcemap: true, + outdir: 'out/', + logLevel: 'info', +}; + +const ctx = await esbuild.context({ ...options, plugins }); +await ctx.rebuild(); +await ctx.watch(); +const localServer = await ctx.serve({ servedir: './', host: '127.0.0.1' }); +createProxyServer(localServer); diff --git a/examples/xc-transactor/index.html b/examples/xc-transactor/index.html new file mode 100644 index 00000000..e230e6f4 --- /dev/null +++ b/examples/xc-transactor/index.html @@ -0,0 +1,19 @@ + + + + + + + XC Transactor POC + + + + + + + + diff --git a/examples/xc-transactor/package.json b/examples/xc-transactor/package.json new file mode 100644 index 00000000..15db9b33 --- /dev/null +++ b/examples/xc-transactor/package.json @@ -0,0 +1,16 @@ +{ + "name": "xc-transactor", + "private": true, + "type": "module", + "main": "out/index.js", + "scripts": { + "dev": "node ./esbuild.dev.mjs" + }, + "devDependencies": { + "esbuild": "^0.25.0", + "esbuild-plugin-wasm": "^1.0.0" + }, + "dependencies": { + "@galacticcouncil/xc": "^0.4.0" + } +} diff --git a/examples/xc-transactor/src/abi.ts b/examples/xc-transactor/src/abi.ts new file mode 100644 index 00000000..3c63629c --- /dev/null +++ b/examples/xc-transactor/src/abi.ts @@ -0,0 +1,91 @@ +export const XCM_TRANSACTOR_ABI = [ + { + inputs: [ + { + components: [ + { internalType: 'uint8', name: 'parents', type: 'uint8' }, + { internalType: 'bytes[]', name: 'interior', type: 'bytes[]' }, + ], + internalType: 'struct Multilocation', + name: 'dest', + type: 'tuple', + }, + { + components: [ + { internalType: 'uint8', name: 'parents', type: 'uint8' }, + { internalType: 'bytes[]', name: 'interior', type: 'bytes[]' }, + ], + internalType: 'struct Multilocation', + name: 'feeLocation', + type: 'tuple', + }, + { + components: [ + { internalType: 'uint64', name: 'refTime', type: 'uint64' }, + { internalType: 'uint64', name: 'proofSize', type: 'uint64' }, + ], + internalType: 'struct Weight', + name: 'transactRequiredWeightAtMost', + type: 'tuple', + }, + { internalType: 'bytes', name: 'call', type: 'bytes' }, + { internalType: 'uint256', name: 'feeAmount', type: 'uint256' }, + { + components: [ + { internalType: 'uint64', name: 'refTime', type: 'uint64' }, + { internalType: 'uint64', name: 'proofSize', type: 'uint64' }, + ], + internalType: 'struct Weight', + name: 'overallWeight', + type: 'tuple', + }, + { internalType: 'bool', name: 'refund', type: 'bool' }, + ], + name: 'transactThroughSignedMultilocation', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'uint8', name: 'parents', type: 'uint8' }, + { internalType: 'bytes[]', name: 'interior', type: 'bytes[]' }, + ], + internalType: 'struct Multilocation', + name: 'dest', + type: 'tuple', + }, + { internalType: 'address', name: 'feeLocationAddress', type: 'address' }, + { + components: [ + { internalType: 'uint64', name: 'refTime', type: 'uint64' }, + { internalType: 'uint64', name: 'proofSize', type: 'uint64' }, + ], + internalType: 'struct Weight', + name: 'transactRequiredWeightAtMost', + type: 'tuple', + }, + { internalType: 'bytes', name: 'call', type: 'bytes' }, + { internalType: 'uint256', name: 'feeAmount', type: 'uint256' }, + { + components: [ + { internalType: 'uint64', name: 'refTime', type: 'uint64' }, + { internalType: 'uint64', name: 'proofSize', type: 'uint64' }, + ], + internalType: 'struct Weight', + name: 'overallWeight', + type: 'tuple', + }, + { internalType: 'bool', name: 'refund', type: 'bool' }, + ], + name: 'transactThroughSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; + +export const XCM_TRANSACTOR_V3 = + '0x0000000000000000000000000000000000000817' as `0x${string}`; diff --git a/examples/xc-transactor/src/index.ts b/examples/xc-transactor/src/index.ts new file mode 100644 index 00000000..160930b8 --- /dev/null +++ b/examples/xc-transactor/src/index.ts @@ -0,0 +1,115 @@ +import { + acc, + addr, + Abi, + AnyEvmChain, + CallType, + Precompile, +} from '@galacticcouncil/xc-core'; +import { EvmCall } from '@galacticcouncil/xc-sdk'; +import { encodeFunctionData } from 'viem'; + +import { XCM_TRANSACTOR_ABI, XCM_TRANSACTOR_V3 } from './abi'; +import { sign } from './signers'; +import { ctx } from './setup'; +import { encodeParachainJunction } from './utils'; + +const { config } = ctx; + +// Define transfer constraints +const srcChain = config.getChain('moonbeam') as AnyEvmChain; + +const HYDRATION_PARACHAIN_ID = 2034; +const MOONBEAM_PARACHAIN_ID = 2004; + +// Define source & dest accounts +const srcAddr = '0x03F067F5174DF6E4E49285D32d9Ce1615711BCe9'; +// 7L53bUTBopuwFt3mKUfmkzgGLayYa1Yvn1hAg9v5UMrQzTfh + +// HDX XC-20 address on Moonbeam +const HDX_XC20 = '0xffffffff345dc44ddae98df024eb494321e73fcc' as `0x${string}`; + +// Compute derived origin account on Hydration +const derivedAccount = acc.getMultilocationDerivatedAccount( + MOONBEAM_PARACHAIN_ID, + srcAddr, + 1, // parents = 1 (sibling parachain) + false +); +const derivedPubKey = addr.Ss58Addr.getPubKey(derivedAccount) as `0x${string}`; + +console.log('Moonbeam sender:', srcAddr); +console.log('Derived origin account on Hydration:', derivedAccount); + +// Subcall 1: Fund derived account via PolkadotXcm.transferAssetsToPara32 +// Sends HDX from Moonbeam to the derived account on Hydration. +// HDX is used as fee asset on Hydration for the subsequent transact call. +const fundCallData = encodeFunctionData({ + abi: Abi.PolkadotXcm, + functionName: 'transferAssetsToPara32', + args: [ + HYDRATION_PARACHAIN_ID, + derivedPubKey, + [{ asset: HDX_XC20, amount: 10_000_000_000_000n }], // 10 HDX (12 decimals) + 0, // feeAssetItem + ], +}); + +// Subcall 2: Remote transact via XcmTransactor precompile +// Executes balances.transferKeepAlive on Hydration from the derived account. +// Fees are paid in HDX (funded by subcall 1). + +// Pre-encoded Hydration call: Balances.transfer_keep_alive(dest, 100000) +// Encoded via Polkadot.js Apps, can be replaced with any Hydration extrinsic +const encodedHydrationCall = + '0x070382fb02afe02fe5d6c793145a75e6860c4e206682c3ab395a9d2a941eb7c0d30d0b00a0724e1809'; + +const transactCallData = encodeFunctionData({ + abi: XCM_TRANSACTOR_ABI, + functionName: 'transactThroughSigned', + args: [ + // dest: Hydration (parachain 2034) + { parents: 1, interior: [encodeParachainJunction(HYDRATION_PARACHAIN_ID)] }, + // feeLocationAddress: HDX XC-20 on Moonbeam + HDX_XC20, + // transactRequiredWeightAtMost + { refTime: 1_000_000_000n, proofSize: 50_000n }, + // call (SCALE-encoded Hydration extrinsic) + encodedHydrationCall as `0x${string}`, + // feeAmount: 5 HDX (pays XCM execution fees on Hydration, 12 decimals) + 5_000_000_000_000n, + // overallWeight: overall weight for full XCM program + { refTime: 2_000_000_000n, proofSize: 100_000n }, + // refund surplus + true, + ], +}); + +// Batch both subcalls via Moonbeam Batch precompile (0x0808) +const batchCallData = encodeFunctionData({ + abi: Abi.Batch, + functionName: 'batchAll', + args: [ + [Precompile.PolkadotXcm, XCM_TRANSACTOR_V3], + [0n, 0n], + [fundCallData, transactCallData], + [], + ], +}); + +// Dump call info +console.log('Encoded Hydration call:', encodedHydrationCall); +console.log('Batched calldata:', batchCallData); + +const call: EvmCall = { + from: srcAddr, + data: batchCallData, + type: CallType.Evm, + to: Precompile.Batch as `0x${string}`, + dryRun: async () => undefined, +}; + +console.log(call); + +// Sign & send +await sign(call, srcChain); diff --git a/examples/xc-transactor/src/setup.ts b/examples/xc-transactor/src/setup.ts new file mode 100644 index 00000000..83721442 --- /dev/null +++ b/examples/xc-transactor/src/setup.ts @@ -0,0 +1,3 @@ +import { createXcContext } from '@galacticcouncil/xc'; + +export const ctx = await createXcContext(); diff --git a/examples/xc-transactor/src/signers/evm.ts b/examples/xc-transactor/src/signers/evm.ts new file mode 100644 index 00000000..d026a15d --- /dev/null +++ b/examples/xc-transactor/src/signers/evm.ts @@ -0,0 +1,51 @@ +import { h160 } from '@galacticcouncil/common'; +import { AnyChain, AnyEvmChain } from '@galacticcouncil/xc-core'; +import { Call, EvmCall } from '@galacticcouncil/xc-sdk'; + +const { H160 } = h160; + +export async function signAndSend( + call: Call, + chain: AnyChain, + onTransactionSend: (hash: string | null) => void, + onTransactionReceipt: (receipt: any) => void, + onError: (error: unknown) => void +) { + const evmChain = chain as AnyEvmChain; + const client = evmChain.evmClient; + const account = H160.fromAny(call.from); + + const provider = client.getProvider(); + const signer = client.getSigner(account); + + await signer.switchChain({ id: client.chain.id }); + await signer.request({ method: 'eth_requestAccounts' }); + + const { data, to, value } = call as EvmCall; + const estGas = await provider.estimateGas({ + account: account as `0x${string}`, + data: data as `0x${string}`, + to: to as `0x${string}`, + value: value, + }); + console.log('Est gas: ' + estGas); + + const txHash = await signer.sendTransaction({ + account: account as `0x${string}`, + chain: client.chain, + data: data as `0x${string}`, + to: to as `0x${string}`, + value: value, + }); + + onTransactionSend(txHash); + provider + .waitForTransactionReceipt({ + hash: txHash, + }) + .then((receipt) => onTransactionReceipt(receipt)) + .catch((error: any) => { + console.log(error); + onError(error); + }); +} diff --git a/examples/xc-transactor/src/signers/index.ts b/examples/xc-transactor/src/signers/index.ts new file mode 100644 index 00000000..b6674521 --- /dev/null +++ b/examples/xc-transactor/src/signers/index.ts @@ -0,0 +1,20 @@ +import { AnyChain } from '@galacticcouncil/xc-core'; +import { Call } from '@galacticcouncil/xc-sdk'; + +import * as evm from './evm'; + +export async function sign(call: Call, chain: AnyChain) { + evm.signAndSend( + call, + chain, + (hash) => { + console.log('TxHash: ' + hash); + }, + (receipt) => { + console.log(receipt); + }, + (error) => { + console.error(error); + } + ); +} diff --git a/examples/xc-transactor/src/utils.ts b/examples/xc-transactor/src/utils.ts new file mode 100644 index 00000000..fdb31b0f --- /dev/null +++ b/examples/xc-transactor/src/utils.ts @@ -0,0 +1,17 @@ +// Encode Parachain junction for Moonbeam Multilocation struct +// Selector 0x00 + 4-byte parachain ID (big-endian) +export function encodeParachainJunction(id: number): `0x${string}` { + return `0x00${id.toString(16).padStart(8, '0')}`; +} + +// Encode PalletInstance junction for Moonbeam Multilocation struct +// Selector 0x04 + 1-byte pallet index +export function encodePalletInstanceJunction(index: number): `0x${string}` { + return `0x04${index.toString(16).padStart(2, '0')}`; +} + +// Encode GeneralIndex junction for Moonbeam Multilocation struct +// Selector 0x05 + 16-byte index (big-endian u128) +export function encodeGeneralIndexJunction(index: number): `0x${string}` { + return `0x05${index.toString(16).padStart(32, '0')}`; +} diff --git a/examples/xc-transactor/tsconfig.json b/examples/xc-transactor/tsconfig.json new file mode 100644 index 00000000..ace9d432 --- /dev/null +++ b/examples/xc-transactor/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "src", + "noImplicitAny": false + }, + "include": ["src/**/*.ts"] +} diff --git a/package-lock.json b/package-lock.json index 28dae5a9..7ef6fb9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,15 @@ "esbuild": "^0.25.0" } }, + "examples/xc-transactor": { + "dependencies": { + "@galacticcouncil/xc": "^0.4.0" + }, + "devDependencies": { + "esbuild": "^0.25.0", + "esbuild-plugin-wasm": "^1.0.0" + } + }, "examples/xc-transfer": { "dependencies": { "@galacticcouncil/xc": "^0.4.0", @@ -14764,6 +14773,10 @@ "resolved": "integration-tests/xc-test", "link": true }, + "node_modules/xc-transactor": { + "resolved": "examples/xc-transactor", + "link": true + }, "node_modules/xc-transfer": { "resolved": "examples/xc-transfer", "link": true