From af74007bbf5c2926b1c396c5c05d05ec381584cd Mon Sep 17 00:00:00 2001 From: Matias Volpe Date: Tue, 21 Mar 2023 10:02:30 -0300 Subject: [PATCH] feat: add xcm_reserve_transfer example --- examples/xcm_reserve_transfer.ts | 282 ++++++++++++++++++++++++++---- fluent/ExtrinsicRune.ts | 6 + patterns/signature/polkadot.ts | 6 +- rpc/ws.ts | 6 +- scale_info/overrides/overrides.ts | 8 + 5 files changed, 269 insertions(+), 39 deletions(-) diff --git a/examples/xcm_reserve_transfer.ts b/examples/xcm_reserve_transfer.ts index 0be2d2905..62a2c8371 100644 --- a/examples/xcm_reserve_transfer.ts +++ b/examples/xcm_reserve_transfer.ts @@ -1,51 +1,263 @@ -import { alice } from "capi" import { - ParasSudoWrapper, - Sudo, - System, - types as relayTypes, -} from "zombienet/xcm_playground.toml/alice/@latest/mod.js" -import { - Assets, - // types as statemineTypes, -} from "zombienet/xcm_playground.toml/statemine-collator01/@latest/mod.js" -// import { -// System, -// types, -// XcmPallet, -// } from "zombienet/xcm_playground.toml/trappist-collator01/@latest/mod.js" - -const result = await System.Account.value(alice.publicKey).run() - -console.log(result) - -function setup() { - const forceCreateAsset = Assets.forceCreate({ - id: 1, + alice, + bob, + Chain, + ChainRune, + Era, + hex, + Rune, + RunicArgs, + SignatureData, + ss58, + ValueRune, +} from "capi" +import * as Rococo from "zombienet/xcm_playground.toml/alice/@latest/mod.js" +import * as Statemine from "zombienet/xcm_playground.toml/statemine-collator01/@latest/mod.js" +import * as Trappist from "zombienet/xcm_playground.toml/trappist-collator01/@latest/mod.js" +import { delay } from "../deps/std/async.ts" +import { SignatureProps } from "../patterns/signature/polkadot.ts" + +const ASSET_ID = 31 +const TRAPPIST_ASSET_ID = ASSET_ID +const ASSET_AMOUNT_TO_MINT = 100000000000000n +const ASSET_AMOUNT_TO_SEND = 10000000000000n + +// await sanity() +await setup() +await doReserveTransfer() + +async function setup() { + const forceCreateAssetTransactEncodedCall = await Statemine.Assets.forceCreate({ + id: ASSET_ID, isSufficient: true, minBalance: 1n, owner: alice.address, - }) + }).call + // TODO: .map() to relayTypes.xcm.v2.Instruction.Transact or t.xcm.double_encoded.DoubleEncoded + .run() - const createAsset = Sudo.sudo({ - call: ParasSudoWrapper.sudoQueueDownwardXcm({ + const createReserveAsset = Rococo.Sudo.sudo({ + call: Rococo.ParasSudoWrapper.sudoQueueDownwardXcm({ id: 1000, - xcm: { + xcm: Rune.rec({ type: "V2", - value: [ // convert to Array - relayTypes.xcm.v2.Instruction.Transact({ + value: Rune.array([ + Rococo.types.xcm.v2.Instruction.Transact({ originType: "Superuser", requireWeightAtMost: 1000000000n, - call: forceCreateAsset, // convert to t.xcm.double_encoded.DoubleEncoded + // TODO: convert to t.xcm.double_encoded.DoubleEncoded + call: { + encoded: forceCreateAssetTransactEncodedCall, + }, }), - ], - }, + ]), + }), + }), + }) + + await createReserveAsset + .signed(signature({ sender: alice })) + .sent() + .dbgStatus("ParasSudoWrapper.sudoQueueDownwardXcm") + .finalizedEvents() + .run() + + await waitFor(async () => (await Statemine.Assets.Asset.value(ASSET_ID).run()) !== undefined) // wait for asset id created + console.log("asset created", await Statemine.Assets.Asset.value(ASSET_ID).run()) + + const mintAsset = Statemine.Assets.mint({ + id: ASSET_ID, + amount: ASSET_AMOUNT_TO_MINT, + beneficiary: bob.address, + }) + + await mintAsset + .signed(signature({ sender: alice })) + .sent() + .dbgStatus("mint reserve asset") + .finalizedEvents() + .run() + + console.log("reserve asset minted", await Statemine.Assets.Asset.value(ASSET_ID).run()) + console.log( + "bob reserve asset balance", + await Statemine.Assets.Account.value([ASSET_ID, bob.publicKey]).run(), + ) + + const createDerivedAsset = Trappist.Sudo.sudo({ + call: Trappist.Assets.forceCreate({ + id: TRAPPIST_ASSET_ID, + isSufficient: false, + minBalance: 1n, + owner: alice.address, }), }) - return createAsset + await createDerivedAsset + .signed(signature({ sender: alice })) + .sent() + .dbgStatus("create derived asset") + .finalizedEvents() + .run() + + const { + v1: { + junction: { Junction }, + multilocation: { Junctions }, + }, + } = Trappist.types.xcm + // TODO: batch with createDerivedAsset + const registerReserveAsset = Trappist.Sudo.sudo({ + call: Trappist.AssetRegistry.registerReserveAsset({ + assetId: TRAPPIST_ASSET_ID, + assetMultiLocation: Rune.rec({ + parents: 1, + interior: Junctions.X3( + // TODO: find parachain id + Junction.Parachain(1000), + Junction.PalletInstance((await Statemine.Assets.pallet.run()).id), + Junction.GeneralIndex(BigInt(ASSET_ID)), + ), + }), + }), + }) + + await registerReserveAsset + .signed(signature({ sender: alice })) + .sent() + .dbgStatus("register reserve asset") + .finalizedEvents() + .run() +} + +async function doReserveTransfer() { + const { + VersionedMultiLocation, + VersionedMultiAssets, + v0: { junction: { NetworkId } }, + v1: { + junction: { Junction }, + multilocation: { Junctions }, + multiasset: { AssetId, Fungibility }, + }, + v2: { WeightLimit }, + } = Statemine.types.xcm + const limitedReserveTransferAssets = Statemine.PolkadotXcm.limitedReserveTransferAssets({ + dest: VersionedMultiLocation.V1(Rune.rec({ + parents: 1, + interior: Junctions.X1( + // TODO: find parachain id + Junction.Parachain(2000), + ), + })), + beneficiary: VersionedMultiLocation.V1(Rune.rec({ + parents: 0, + interior: Junctions.X1( + Junction.AccountId32({ + network: NetworkId.Any(), + id: bob.publicKey, + }), + ), + })), + assets: VersionedMultiAssets.V1(Rune.array([Rune.rec({ + id: AssetId.Concrete(Rune.rec({ + parents: 0, + interior: Junctions.X2( + Junction.PalletInstance((await Statemine.Assets.pallet.run()).id), + Junction.GeneralIndex(BigInt(ASSET_ID)), + ), + })), + fun: Fungibility.Fungible(ASSET_AMOUNT_TO_SEND), + })])), + feeAssetItem: 0, + weightLimit: WeightLimit.Unlimited(), + }) + + await limitedReserveTransferAssets + .signed(signature({ sender: bob })) + .sent() + .dbgStatus("limitedReserveTransferAssets") + .finalizedEvents() + .run() + + await waitFor(async () => + await Trappist.Assets.Account.value([TRAPPIST_ASSET_ID, bob.publicKey]).run() !== undefined + ) // wait for bob balance to update + console.log( + "Trappist Bob asset balance", + await Trappist.Assets.Account.value([TRAPPIST_ASSET_ID, bob.publicKey]).run(), + ) +} + +async function waitFor( + fn: () => Promise, + delay_ = 1000, +) { + while (true) { + const result = await fn() + if (result) break + await delay(delay_) + } +} + +async function sanity() { + console.log(await Rococo.System.Account.value(alice.publicKey).run()) + console.log(await Statemine.System.Account.value(alice.publicKey).run()) + console.log(await Trappist.System.Account.value(alice.publicKey).run()) } -if (import.meta.main) { - await setup().run() +function signature(_props: RunicArgs>) { + return (chain: ChainRune) => { + const props = RunicArgs.resolve(_props) + // @ts-ignore FIXME: + const addrPrefix = chain.addressPrefix() + const versions = chain.pallet("System").constant("Version").decoded + const specVersion = versions.access("specVersion") + const transactionVersion = versions.access("transactionVersion") + // TODO: create union rune (with `matchTag` method) and utilize here + // TODO: MultiAddress conversion utils + const senderSs58 = Rune + .tuple([addrPrefix, props.sender]) + .map(([addrPrefix, sender]) => { + switch (sender.address.type) { + case "Id": { + return ss58.encode(addrPrefix, sender.address.value) + } + default: { + throw new Error("unimplemented") + } + } + }) + .throws(ss58.InvalidPayloadLengthError) + const nonce = Rune.resolve(props.nonce) + .unhandle(undefined) + .rehandle(undefined, () => chain.connection.call("system_accountNextIndex", senderSs58)) + const genesisHashHex = chain.connection.call("chain_getBlockHash", 0).unsafeAs() + .into(ValueRune) + const genesisHash = genesisHashHex.map(hex.decode) + const checkpointHash = Rune.tuple([props.checkpoint, genesisHashHex]).map(([a, b]) => a ?? b) + .map(hex.decode) + const mortality = Rune.resolve(props.mortality).map((x) => x ?? Era.Immortal) + const tip = Rune.resolve(props.tip).map((x) => x ?? 0n) + return Rune.rec({ + sender: props.sender, + extra: Rune.rec({ + CheckMortality: mortality, + CheckNonce: nonce, + ChargeTransactionPayment: tip, + ChargeAssetTxPayment: Rune.rec({ + // FIXME: + // assetId: props.assetId, + tip: tip, + }), + }), + additional: Rune.rec({ + CheckSpecVersion: specVersion, + CheckTxVersion: transactionVersion, + CheckGenesis: genesisHash, + CheckMortality: checkpointHash, + }), + // @ts-ignore FIXME: + }) satisfies Rune, unknown> + } } diff --git a/fluent/ExtrinsicRune.ts b/fluent/ExtrinsicRune.ts index a4820ac61..d69a19070 100644 --- a/fluent/ExtrinsicRune.ts +++ b/fluent/ExtrinsicRune.ts @@ -22,6 +22,7 @@ export type SignatureDataFactory = ( export class ExtrinsicRune extends Rune, U> { hash + call constructor(_prime: ExtrinsicRune["_prime"], readonly chain: ChainRune) { super(_prime) @@ -31,6 +32,11 @@ export class ExtrinsicRune extends Rune blake2_256.$hash(x)) .into(CodecRune) .encoded(this) + this.call = this.chain + .into(ValueRune) + .access("metadata", "extrinsic", "call") + .into(CodecRune) + .encoded(this) } signed(signatureFactory: SignatureDataFactory) { diff --git a/patterns/signature/polkadot.ts b/patterns/signature/polkadot.ts index a9a8629b7..d11c92080 100644 --- a/patterns/signature/polkadot.ts +++ b/patterns/signature/polkadot.ts @@ -5,8 +5,8 @@ import { $, hex, ss58, ValueRune } from "../../mod.ts" import { Rune, RunicArgs } from "../../rune/Rune.ts" import { Era } from "../../scale_info/overrides/Era.ts" -export interface SignatureProps { - sender: ExtrinsicSender +export interface SignatureProps { + sender: ExtrinsicSender checkpoint?: string mortality?: Era nonce?: number @@ -28,7 +28,7 @@ export interface PolkadotSignatureChain extends AddressPrefixChain { } } -export function signature(_props: RunicArgs) { +export function signature(_props: RunicArgs>) { return (chain: ChainRune) => { const props = RunicArgs.resolve(_props) const addrPrefix = chain.addressPrefix() diff --git a/rpc/ws.ts b/rpc/ws.ts index 9a94ac792..71c4be799 100644 --- a/rpc/ws.ts +++ b/rpc/ws.ts @@ -50,6 +50,10 @@ export class WsConnection extends Connection { } close() { - this.ws.close() + console.log("will close", this.ws.readyState, this.url) + // console.trace() + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.close() + } } } diff --git a/scale_info/overrides/overrides.ts b/scale_info/overrides/overrides.ts index 740744f37..405992b9b 100644 --- a/scale_info/overrides/overrides.ts +++ b/scale_info/overrides/overrides.ts @@ -30,6 +30,14 @@ export const overrides: Record Codec "frame_support::traits::misc::WrapperKeepOpaque": (ty, visit) => { return $.lenPrefixed(visit(ty.params[0]!.ty!)) }, + "xcm::double_encoded::DoubleEncoded": (ty, visit) => { + console.log("xcm::double_encoded::DoubleEncoded") + return $.lenPrefixed(visit(ty.params[0]!.ty!)) + }, + "xcm::DoubleEncoded": (ty, visit) => { + console.log("xcm::DoubleEncoded") + return $.lenPrefixed(visit(ty.params[0]!.ty!)) + }, "sp_runtime::generic::era::Era": () => { return $era },