From b77f4579cbea6cfb0a3069366d11dcdaf5bc977c Mon Sep 17 00:00:00 2001 From: alecdwm Date: Wed, 14 Aug 2024 04:07:55 +0000 Subject: [PATCH] wip: papi for tx encoding --- .../src/ui/domains/SendFunds/useSendFunds.ts | 6 +- .../src/modules/SubstrateAssetsModule.ts | 8 +- .../src/modules/SubstrateEquilibriumModule.ts | 12 +- .../modules/SubstrateForeignAssetsModule.ts | 173 +++--------------- .../modules/SubstrateNativeModule/index.ts | 8 +- .../src/modules/SubstratePsp22Module.ts | 2 +- .../src/modules/SubstrateTokensModule.ts | 146 ++------------- .../src/modules/util/detectTransferMethod.ts | 2 +- .../src/domains/transfers/handler.ts | 4 +- .../src/domains/transfers/types.ts | 2 +- packages/scale/package.json | 1 + packages/scale/src/util/index.ts | 1 + packages/scale/src/util/serdePapi.spec.ts | 31 ++++ packages/scale/src/util/serdePapi.ts | 56 ++++++ pnpm-lock.yaml | 3 + 15 files changed, 148 insertions(+), 307 deletions(-) create mode 100644 packages/scale/src/util/serdePapi.spec.ts create mode 100644 packages/scale/src/util/serdePapi.ts diff --git a/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts b/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts index f474581b0..72ed5cf53 100644 --- a/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts +++ b/apps/extension/src/ui/domains/SendFunds/useSendFunds.ts @@ -256,10 +256,10 @@ const useSendFundsProvider = () => { ) const method: AssetTransferMethod = sendMax - ? "transferAll" + ? "transfer_all" : allowReap - ? "transferAllowDeath" - : "transferKeepAlive" + ? "transfer_allow_death" + : "transfer_keep_alive" const { evmTransaction, evmInvalidTxError } = useEvmTransaction( tokenId, diff --git a/packages/balances/src/modules/SubstrateAssetsModule.ts b/packages/balances/src/modules/SubstrateAssetsModule.ts index c74100ca8..91d5f412f 100644 --- a/packages/balances/src/modules/SubstrateAssetsModule.ts +++ b/packages/balances/src/modules/SubstrateAssetsModule.ts @@ -63,7 +63,7 @@ export type SubAssetsTransferParams = NewTransferParamsType<{ specVersion: number transactionVersion: number tip?: string - transferMethod: "transfer" | "transferKeepAlive" | "transferAll" + transferMethod: "transfer" | "transfer_keep_alive" | "transfer_all" userExtensions?: ExtDef }> @@ -252,10 +252,10 @@ export const SubAssetsModule: NewBalanceModule< const id = token.assetId - const pallet = "assets" + const pallet = "Assets" const method = - // the assets pallet has no transferAll method - transferMethod === "transferAll" ? "transfer" : transferMethod + // the assets pallet has no transfer_all method + transferMethod === "transfer_all" ? "transfer" : transferMethod const args = { id, target: { Id: to }, amount } const unsigned = defineMethod( diff --git a/packages/balances/src/modules/SubstrateEquilibriumModule.ts b/packages/balances/src/modules/SubstrateEquilibriumModule.ts index 505f64ca1..4e7f3276d 100644 --- a/packages/balances/src/modules/SubstrateEquilibriumModule.ts +++ b/packages/balances/src/modules/SubstrateEquilibriumModule.ts @@ -66,7 +66,7 @@ export type SubEquilibriumTransferParams = NewTransferParamsType<{ specVersion: number transactionVersion: number tip?: string - transferMethod: "transfer" | "transferKeepAlive" | "transferAll" + transferMethod: "transfer" | "transfer_keep_alive" | "transfer_all" userExtensions?: ExtDef }> @@ -242,13 +242,13 @@ export const SubEquilibriumModule: NewBalanceModule< const { assetId } = token - const pallet = "eqBalances" + const pallet = "EqBalances" const method = - transferMethod === "transferAll" - ? // the eqBalances pallet has no transferAll method + transferMethod === "transfer_all" + ? // the eqBalances pallet has no transfer_all method "transfer" - : transferMethod === "transferKeepAlive" - ? // the eqBalances pallet has no transferKeepAlive method + : transferMethod === "transfer_keep_alive" + ? // the eqBalances pallet has no transfer_keep_alive method "transfer" : "transfer" const args = { asset: assetId, to, value: amount } diff --git a/packages/balances/src/modules/SubstrateForeignAssetsModule.ts b/packages/balances/src/modules/SubstrateForeignAssetsModule.ts index 683ad15c6..bf7f8a0ed 100644 --- a/packages/balances/src/modules/SubstrateForeignAssetsModule.ts +++ b/packages/balances/src/modules/SubstrateForeignAssetsModule.ts @@ -16,8 +16,9 @@ import { encodeMetadata, encodeStateKey, getDynamicBuilder, + papiParse, } from "@talismn/scale" -import { Binary, Enum } from "polkadot-api" +import { Binary } from "polkadot-api" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../BalanceModule" import log from "../log" @@ -64,7 +65,7 @@ export type SubForeignAssetsTransferParams = NewTransferParamsType<{ specVersion: number transactionVersion: number tip?: string - transferMethod: "transfer" | "transferKeepAlive" | "transferAll" + transferMethod: "transfer" | "transfer_keep_alive" | "transfer_all" userExtensions?: ExtDef }> @@ -118,7 +119,7 @@ export const SubForeignAssetsModule: NewBalanceModule< try { const onChainId = (() => { try { - return JSON.parse(tokenConfig.onChainId) + return papiParse(tokenConfig.onChainId) } catch (error) { return tokenConfig.onChainId } @@ -126,15 +127,8 @@ export const SubForeignAssetsModule: NewBalanceModule< if (onChainId === undefined) continue - const parsedOnChainId = parseOnChainId(chainId, onChainId) - - const assetStateKey = tryEncode(assetCoder, parsedOnChainId) - const metadataStateKey = tryEncode(metadataCoder, parsedOnChainId) - - if (assetStateKey === null || metadataStateKey === null) - throw new Error( - `Failed to encode stateKey for foreign token ${onChainId} on chain ${chainId}` - ) + const assetStateKey = assetCoder.enc(onChainId) + const metadataStateKey = metadataCoder.enc(onChainId) type AssetResult = { accounts?: number @@ -199,7 +193,7 @@ export const SubForeignAssetsModule: NewBalanceModule< } catch (error) { log.error( `Failed to build substrate-foreignassets token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, - error + (error as Error)?.message ?? error ) continue } @@ -260,19 +254,25 @@ export const SubForeignAssetsModule: NewBalanceModule< const { genesisHash } = chain - const id = (() => { + const onChainId = (() => { try { - return JSON.parse(token.onChainId) + return papiParse(token.onChainId) } catch (error) { return token.onChainId } })() - const pallet = "foreignAssets" + const pallet = "ForeignAssets" const method = - // the foreignAssets pallet has no transferAll method - transferMethod === "transferAll" ? "transfer" : transferMethod - const args = { id, target: { Id: to }, amount } + // the ForeignAssets pallet has no transfer_all method + transferMethod === "transfer_all" ? "transfer" : transferMethod + const args = { id: onChainId, target: { Id: to }, amount } + + const { metadata } = decodeMetadata(metadataRpc) + if (metadata === undefined) throw new Error("Unable to decode metadata") + + const scaleBuilder = getDynamicBuilder(metadata) + scaleBuilder.buildCall(pallet, method) const unsigned = defineMethod( { @@ -348,18 +348,16 @@ async function buildQueries( const scaleCoder = chainStorageCoders.get(chainId)?.storage const onChainId = (() => { try { - // `as string` doesn't matter here because we catch it if it throws - return JSON.parse(token.onChainId as string) + return papiParse(token.onChainId) } catch (error) { return token.onChainId } })() - const parsedOnChainId = parseOnChainId(chainId, onChainId) const stateKey = encodeStateKey( scaleCoder, `Invalid address / token onChainId in ${chainId} storage query ${address} / ${token.onChainId}\n` + - `onChainId parsed as: '${JSON.stringify(parsedOnChainId)}'`, + `onChainId parsed as: '${JSON.stringify(onChainId)}'`, address, onChainId ) @@ -425,132 +423,3 @@ async function buildQueries( }) }) } - -// TODO: Dedupe with SubstrateTokensModule parseOnChainId -// TODO: See if this can be upstreamed / is actually necessary. -// There might be a better way to construct enums with polkadot-api. -// -/** - * For the substrate-tokens module, we configure the `onChainId` field in chaindata to tell the module how to query each token. - * These queries are made to the tokens pallet. - * E.g. api.query.Tokens.Account(accountAddress, parseOnChainId(JSON.parse(onChainId))) - * - * The `onChainId` field on chaindata must be a JSON-parseable string, but for some SCALE types (especially the Enum type) we must - * use specific `polkadot-api` classes to handle SCALE-encoding the statekey. - * - * Some examples: - * Input: `5` - * Output: `5` - * - * Input: `{ DexShare: [{ Token: "ACA" }, { Token: "AUSD" }] }` - * Output: `Enum("DexShare", [Enum("Token", Enum("ACA")), Enum("Token", Enum("AUSD"))])` - * - * Input: `{ LiquidCrowdloan: 13 }` - * Output: `Enum("LiquidCrowdloan", 13)` - * - * Input: `{ Erc20: "0x07df96d1341a7d16ba1ad431e2c847d978bc2bce" }` - * Output: `Enum("Erc20", Binary.fromHex("0x07df96d1341a7d16ba1ad431e2c847d978bc2bce"))` - */ -const parseOnChainId = (chainId: string, onChainId: unknown, recurse = false): unknown => { - // haven't seen this used by any chains before, - // but it's technically possible for `null` to slip through the following `typeof onChainId === 'object'` checks, - // so we handle it explicitly here: return is as-is - if (onChainId === null) return onChainId - - // numbers should not be modified - // TODO: Handle int/bigint differentiation - if (typeof onChainId === "number") return onChainId - - // arrays, objects (enums) and strings (binary strings, as well as enums with an undefined value) need to be handled - // everything else should not be modified - if (typeof onChainId !== "object" && typeof onChainId !== "string") { - console.log(chainId, "OTHER!", onChainId) - return onChainId - } - - // for arrays, pass each array item through this function - if (Array.isArray(onChainId)) { - console.log(chainId, "ARRAY!", onChainId) - const res = onChainId.map((item) => parseOnChainId(chainId, item, true)) - console.log(chainId, "RESULT!ARRAY!", res) - return res - } - - // for hexadecimal strings, parse as hex into the Binary type - if (typeof onChainId === "string" && onChainId.startsWith("0x")) { - console.log(chainId, "HEX!", onChainId) - return Binary.fromHex(onChainId) - } - - // For any values which haven't already been handled by the previous if statements, - // we are now going to consider them as an Enum. - // - // There are two forms of enums; ones *with* a value and ones *without* a value. - // - // Some examples of enums *without* a value: - // { AUSD: undefined } - // "AUSD" - // { ACA: undefined } - // "ACA" - // - // Some examples of enums *with* a value: - // { Erc20: "0x00......" } // an enum containing a binary string - // { ForeignAsset: 2 } // an enum containing a number - // { DexShare: [{ Token: "ACA" }, { Erc20: "0x00......" }] } // an enum containing an array of enums - // - // Some examples of enums with a value, where the value is another enum: - // { Token: "AUSD" } - // { Token: { AUSD: undefined } } - // { Token: "ACA" } - // { Token: { ACA: undefined } } - // - // NOTE: In the last example, - // `{ Token: { AUSD: undefined } }` - // is the preferred, unambiguous form of specifing an enum within an enum. - // However, - // `{ Token: "AUSD" }` - // is also supported, in order to maintain backwards compatibility with the PJS codebase - // (which is what we had prior to migrating to scale-ts for statekey encoding). - // - // In the future, we might need to drop support for the ambiguous `{ Token: "AUSD" }` form of expressing an enum within an enum, - // as well as the ambiguous `"AUSD"` form, - // because it may prevent us from defining a hypothetical enum which contains a non-hexadecimal string. - // For example, if we needed to support: - // `{ Token: "This is just a string" }` - // to be parsed as - // `Enum("Token", "a string, not an enum")` // an arbitrary string within an enum - // instead of how we currently parse it, which is - // `Enum("Token", Enum("a string, not an enum"))` // an enum within an enum - // - const keys = typeof onChainId === "object" ? Object.keys(onChainId) : [onChainId] - if (keys.length !== 1) { - console.log(chainId, "TOO MANY KEYS!", onChainId) - const res = Object.fromEntries( - Object.entries(onChainId).map(([key, value]) => [key, parseOnChainId(chainId, value, true)]) - ) - console.log(chainId, "TOO MANY KEYS!RESULT!", res) - return res - } - - console.log(chainId, "OBJECT!", onChainId) - const key: string = keys[0] - const value = - typeof onChainId === "object" - ? parseOnChainId(chainId, onChainId[key as keyof typeof onChainId], true) - : undefined - const ret = Enum(key, value) - console.log(chainId, "OBJECT!RESULT!", ret) - // if (!recurse) - // memoLog(chainId, "OBJECT ASSETID", "in:", onChainId, "out:", JSON.stringify(ret), ret) - return ret -} - -type ScaleStorageCoder = ReturnType["buildStorage"]> -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const tryEncode = (scaleCoder: ScaleStorageCoder | undefined, ...args: any[]) => { - try { - return scaleCoder?.enc?.(...args) - } catch { - return null - } -} diff --git a/packages/balances/src/modules/SubstrateNativeModule/index.ts b/packages/balances/src/modules/SubstrateNativeModule/index.ts index 965464927..a4363fb1b 100644 --- a/packages/balances/src/modules/SubstrateNativeModule/index.ts +++ b/packages/balances/src/modules/SubstrateNativeModule/index.ts @@ -139,8 +139,8 @@ declare module "@talismn/balances/plugins" { } } -export type BalancesCommonTransferMethods = "transferKeepAlive" | "transferAll" -export type BalancesTransferMethods = "transferAllowDeath" | BalancesCommonTransferMethods +export type BalancesCommonTransferMethods = "transfer_keep_alive" | "transfer_all" +export type BalancesTransferMethods = "transfer_allow_death" | BalancesCommonTransferMethods export type BalancesLegacyTransferMethods = "transfer" | BalancesCommonTransferMethods export type BalancesAllTransferMethods = BalancesLegacyTransferMethods | BalancesTransferMethods @@ -563,10 +563,10 @@ export const SubNativeModule: NewBalanceModule< const { genesisHash } = chain - const sendAll = transferMethod === "transferAll" + const sendAll = transferMethod === "transfer_all" let method: BalancesAllTransferMethods = transferMethod - if (transferMethod === "transferAllowDeath") { + if (transferMethod === "transfer_allow_death") { try { method = detectTransferMethod(metadataRpc) } catch (cause) { diff --git a/packages/balances/src/modules/SubstratePsp22Module.ts b/packages/balances/src/modules/SubstratePsp22Module.ts index 4c1d931b6..edecf80e6 100644 --- a/packages/balances/src/modules/SubstratePsp22Module.ts +++ b/packages/balances/src/modules/SubstratePsp22Module.ts @@ -59,7 +59,7 @@ export type SubPsp22TransferParams = NewTransferParamsType<{ specVersion: number transactionVersion: number tip?: string - transferMethod: "transfer" | "transferKeepAlive" | "transferAll" + transferMethod: "transfer" | "transfer_keep_alive" | "transfer_all" userExtensions?: ExtDef }> diff --git a/packages/balances/src/modules/SubstrateTokensModule.ts b/packages/balances/src/modules/SubstrateTokensModule.ts index 2e9e8b657..421c2f4a6 100644 --- a/packages/balances/src/modules/SubstrateTokensModule.ts +++ b/packages/balances/src/modules/SubstrateTokensModule.ts @@ -15,8 +15,8 @@ import { decodeScale, encodeMetadata, encodeStateKey, + papiParse, } from "@talismn/scale" -import { Binary, Enum } from "polkadot-api" import { DefaultBalanceModule, NewBalanceModule, NewTransferParamsType } from "../BalanceModule" import log from "../log" @@ -58,6 +58,7 @@ declare module "@talismn/balances/plugins" { } export type SubTokensTransferParams = NewTransferParamsType<{ + /** @depreacted replace with papi tx encoder */ registry: TypeRegistry metadataRpc: `0x${string}` blockHash: string @@ -66,7 +67,7 @@ export type SubTokensTransferParams = NewTransferParamsType<{ specVersion: number transactionVersion: number tip?: string - transferMethod: "transfer" | "transferKeepAlive" | "transferAll" + transferMethod: "transfer" | "transfer_keep_alive" | "transfer_all" userExtensions?: ExtDef }> @@ -199,10 +200,9 @@ export const SubTokensModule: NewBalanceModule< const { genesisHash } = chain - const currencyId = (() => { + const onChainId = (() => { try { - // `as string` doesn't matter here because we catch it if it throws - return JSON.parse(token.onChainId as string) + return papiParse(token.onChainId) } catch (error) { return token.onChainId } @@ -215,15 +215,15 @@ export const SubTokensModule: NewBalanceModule< const currenciesPallet = "currencies" const currenciesMethod = "transfer" - const currenciesArgs = { dest: to, currencyId, amount } + const currenciesArgs = { dest: to, currencyId: onChainId, amount } - const sendAll = transferMethod === "transferAll" + const sendAll = transferMethod === "transfer_all" const tokensPallet = "tokens" const tokensMethod = transferMethod const tokensArgs = sendAll - ? { dest: to, currencyId, keepAlive: false } - : { dest: to, currencyId, amount } + ? { dest: to, currencyId: onChainId, keepAlive: false } + : { dest: to, currencyId: onChainId, amount } const commonDefineMethodFields = { address: from, @@ -329,22 +329,20 @@ async function buildQueries( return addresses.flatMap((address): RpcStateQuery | [] => { const scaleCoder = chainStorageCoders.get(chainId)?.storage - const deserializedOnChainId = (() => { + const onChainId = (() => { try { - // `as string` doesn't matter here because we catch it if it throws - return JSON.parse(token.onChainId as string) + return papiParse(token.onChainId) } catch (error) { return token.onChainId } })() - const assetId = parseOnChainId(chainId, deserializedOnChainId) const stateKey = encodeStateKey( scaleCoder, `Invalid address / token onChainId in ${chainId} storage query ${address} / ${token.onChainId}\n` + - `onChainId parsed as: '${JSON.stringify(assetId)}'`, + `onChainId parsed as: '${JSON.stringify(onChainId)}'`, address, - assetId + onChainId ) if (!stateKey) return [] @@ -387,121 +385,3 @@ async function buildQueries( }) }) } - -// TODO: See if this can be upstreamed / is actually necessary. -// There might be a better way to construct enums with polkadot-api. -// -/** - * For the substrate-tokens module, we configure the `onChainId` field in chaindata to tell the module how to query each token. - * These queries are made to the tokens pallet. - * E.g. api.query.Tokens.Account(accountAddress, parseOnChainId(JSON.parse(onChainId))) - * - * The `onChainId` field on chaindata must be a JSON-parseable string, but for some SCALE types (especially the Enum type) we must - * use specific `polkadot-api` classes to handle SCALE-encoding the statekey. - * - * Some examples: - * Input: `5` - * Output: `5` - * - * Input: `{ DexShare: [{ Token: "ACA" }, { Token: "AUSD" }] }` - * Output: `Enum("DexShare", [Enum("Token", Enum("ACA")), Enum("Token", Enum("AUSD"))])` - * - * Input: `{ LiquidCrowdloan: 13 }` - * Output: `Enum("LiquidCrowdloan", 13)` - * - * Input: `{ Erc20: "0x07df96d1341a7d16ba1ad431e2c847d978bc2bce" }` - * Output: `Enum("Erc20", Binary.fromHex("0x07df96d1341a7d16ba1ad431e2c847d978bc2bce"))` - */ -const parseOnChainId = (chainId: string, onChainId: unknown, recurse = false): unknown => { - // haven't seen this used by any chains before, - // but it's technically possible for `null` to slip through the following `typeof onChainId === 'object'` checks, - // so we handle it explicitly here: return is as-is - if (onChainId === null) return onChainId - - // numbers should not be modified - // TODO: Handle int/bigint differentiation - if (typeof onChainId === "number") return onChainId - - // arrays, objects (enums) and strings (binary strings, as well as enums with an undefined value) need to be handled - // everything else should not be modified - if (typeof onChainId !== "object" && typeof onChainId !== "string") { - console.log(chainId, "OTHER!", onChainId) - return onChainId - } - - // for arrays, pass each array item through this function - if (Array.isArray(onChainId)) { - console.log(chainId, "ARRAY!", onChainId) - const res = onChainId.map((item) => parseOnChainId(chainId, item, true)) - console.log(chainId, "RESULT!ARRAY!", res) - return res - } - - // for hexadecimal strings, parse as hex into the Binary type - if (typeof onChainId === "string" && onChainId.startsWith("0x")) { - console.log(chainId, "HEX!", onChainId) - return Binary.fromHex(onChainId) - } - - // For any values which haven't already been handled by the previous if statements, - // we are now going to consider them as an Enum. - // - // There are two forms of enums; ones *with* a value and ones *without* a value. - // - // Some examples of enums *without* a value: - // { AUSD: undefined } - // "AUSD" - // { ACA: undefined } - // "ACA" - // - // Some examples of enums *with* a value: - // { Erc20: "0x00......" } // an enum containing a binary string - // { ForeignAsset: 2 } // an enum containing a number - // { DexShare: [{ Token: "ACA" }, { Erc20: "0x00......" }] } // an enum containing an array of enums - // - // Some examples of enums with a value, where the value is another enum: - // { Token: "AUSD" } - // { Token: { AUSD: undefined } } - // { Token: "ACA" } - // { Token: { ACA: undefined } } - // - // NOTE: In the last example, - // `{ Token: { AUSD: undefined } }` - // is the preferred, unambiguous form of specifing an enum within an enum. - // However, - // `{ Token: "AUSD" }` - // is also supported, in order to maintain backwards compatibility with the PJS codebase - // (which is what we had prior to migrating to scale-ts for statekey encoding). - // - // In the future, we might need to drop support for the ambiguous `{ Token: "AUSD" }` form of expressing an enum within an enum, - // as well as the ambiguous `"AUSD"` form, - // because it may prevent us from defining a hypothetical enum which contains a non-hexadecimal string. - // For example, if we needed to support: - // `{ Token: "This is just a string" }` - // to be parsed as - // `Enum("Token", "a string, not an enum")` // an arbitrary string within an enum - // instead of how we currently parse it, which is - // `Enum("Token", Enum("a string, not an enum"))` // an enum within an enum - // - const keys = typeof onChainId === "object" ? Object.keys(onChainId) : [onChainId] - if (keys.length !== 1) { - console.log(chainId, "TOO MANY KEYS!", onChainId) - const res = Object.fromEntries( - Object.entries(onChainId).map(([key, value]) => [key, parseOnChainId(chainId, value, true)]) - ) - console.log(chainId, "TOO MANY KEYS!RESULT!", res) - return res - } - - console.log(chainId, "OBJECT!", onChainId) - const key: string = keys[0] - const value = - typeof onChainId === "object" - ? parseOnChainId(chainId, onChainId[key as keyof typeof onChainId], true) - : undefined - const ret = Enum(key, value) - console.log(chainId, "OBJECT!RESULT!", ret) - // if (!recurse) - // memoLog(chainId, "OBJECT ASSETID", "in:", onChainId, "out:", JSON.stringify(ret), ret) - return ret -} diff --git a/packages/balances/src/modules/util/detectTransferMethod.ts b/packages/balances/src/modules/util/detectTransferMethod.ts index a794f08cb..815bffe55 100644 --- a/packages/balances/src/modules/util/detectTransferMethod.ts +++ b/packages/balances/src/modules/util/detectTransferMethod.ts @@ -25,5 +25,5 @@ export const detectTransferMethod = (metadataRpc: `0x${string}`) => { variant.name.eq("transfer") ) !== undefined - return hasDeprecatedTransferCall ? "transfer" : "transferAllowDeath" + return hasDeprecatedTransferCall ? "transfer" : "transfer_allow_death" } diff --git a/packages/extension-core/src/domains/transfers/handler.ts b/packages/extension-core/src/domains/transfers/handler.ts index 2b548dffe..e6011128b 100644 --- a/packages/extension-core/src/domains/transfers/handler.ts +++ b/packages/extension-core/src/domains/transfers/handler.ts @@ -38,7 +38,7 @@ export default class AssetTransferHandler extends ExtensionHandler { toAddress, amount = "0", tip = "0", - method = "transferKeepAlive", + method = "transfer_keep_alive", }: RequestAssetTransfer) { const result = await getPairForAddressSafely(fromAddress, async (pair) => { const token = await chaindataProvider.tokenById(tokenId) @@ -107,7 +107,7 @@ export default class AssetTransferHandler extends ExtensionHandler { toAddress, amount = "0", tip = "0", - method = "transferKeepAlive", + method = "transfer_keep_alive", }: RequestAssetTransfer) { const token = await chaindataProvider.tokenById(tokenId) if (!token) throw new Error(`Invalid tokenId ${tokenId}`) diff --git a/packages/extension-core/src/domains/transfers/types.ts b/packages/extension-core/src/domains/transfers/types.ts index f72479dbd..612737f13 100644 --- a/packages/extension-core/src/domains/transfers/types.ts +++ b/packages/extension-core/src/domains/transfers/types.ts @@ -9,7 +9,7 @@ import { SignerPayloadJSON } from "../signing/types" import { WalletTransactionTransferInfo } from "../transactions" // Asset Transfer Messages -export type AssetTransferMethod = "transferKeepAlive" | "transferAllowDeath" | "transferAll" +export type AssetTransferMethod = "transfer_keep_alive" | "transfer_allow_death" | "transfer_all" export interface RequestAssetTransfer { chainId: ChainId tokenId: TokenId diff --git a/packages/scale/package.json b/packages/scale/package.json index b8c907e52..3d1ac6b15 100644 --- a/packages/scale/package.json +++ b/packages/scale/package.json @@ -30,6 +30,7 @@ "@polkadot-api/substrate-bindings": "^0.6.0", "@polkadot-api/utils": "^0.1.0", "anylogger": "^1.0.11", + "polkadot-api": "^0.6.0", "scale-ts": "^1.6.0" }, "devDependencies": { diff --git a/packages/scale/src/util/index.ts b/packages/scale/src/util/index.ts index 0b5c39956..fa4dea608 100644 --- a/packages/scale/src/util/index.ts +++ b/packages/scale/src/util/index.ts @@ -4,3 +4,4 @@ export * from "./decodeScale" export * from "./encodeMetadata" export * from "./encodeStateKey" export * from "./getMetadataVersion" +export * from "./serdePapi" diff --git a/packages/scale/src/util/serdePapi.spec.ts b/packages/scale/src/util/serdePapi.spec.ts new file mode 100644 index 000000000..046bde240 --- /dev/null +++ b/packages/scale/src/util/serdePapi.spec.ts @@ -0,0 +1,31 @@ +import { papiParse, papiStringify } from "./serdePapi" + +describe("papiParse/papiStringify", () => { + test("test known inputs match expected outputs", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tests: Array<[string | any, string | any]> = [ + [5, "5"], + [5n, '"bigint:5"'], + ["5", "5"], + ['"bigint:12345"', '"bigint:12345"'], + [ + '{"type":"DexShare","value":[{"type":"Token","value":{"type":"ACA"}},{"type":"Token","value":{"type":"AUSD"}}]}', + '{"type":"DexShare","value":[{"type":"Token","value":{"type":"ACA"}},{"type":"Token","value":{"type":"AUSD"}}]}', + ], + ['{"type":"LiquidCrowdloan","value":13}', '{"type":"LiquidCrowdloan","value":13}'], + ['{"type":"NativeToken","value":"bigint:2"}', '{"type":"NativeToken","value":"bigint:2"}'], + [ + '{"type":"Erc20","value":"hex:0x07df96d1341a7d16ba1ad431e2c847d978bc2bce"}', + '{"type":"Erc20","value":"hex:0x07df96d1341a7d16ba1ad431e2c847d978bc2bce"}', + ], + [ + '{"type":"Stellar","value":{"code":"bin:TZS","issuer":"hex:0x34c94b2a4ba9e8b57b22547dcbb30f443c4cb02da3829a89aa1bd4780e4466ba"}}', + '{"type":"Stellar","value":{"code":"hex:0x545a53","issuer":"hex:0x34c94b2a4ba9e8b57b22547dcbb30f443c4cb02da3829a89aa1bd4780e4466ba"}}', + ], + ] + + for (const [input, expectedOutput] of tests) { + expect(papiStringify(papiParse(input))).toEqual(expectedOutput) + } + }) +}) diff --git a/packages/scale/src/util/serdePapi.ts b/packages/scale/src/util/serdePapi.ts new file mode 100644 index 000000000..6458f56c6 --- /dev/null +++ b/packages/scale/src/util/serdePapi.ts @@ -0,0 +1,56 @@ +import { Binary } from "polkadot-api" + +/** + * For the substrate-tokens (and other) modules, we configure the `onChainId` field in chaindata to tell the module how to query each token. + * These queries are made to the tokens pallet. + * E.g. api.query.Tokens.Account(accountAddress, papiParse(onChainId)) + * + * The `onChainId` field on chaindata must be a JSON-parseable string, but for some SCALE types (especially the Binary type) we must + * use specific `polkadot-api` classes to handle SCALE-encoding the statekey. + * + * Some examples: + * Input: `5` + * Output: `5` + * + * Input: `{ type: "DexShare", value: [ { type: "Token", value: { type: "ACA" } }, { type: "Token", value: { type: "AUSD" } } ] }` + * Output: `Enum("DexShare", [Enum("Token", Enum("ACA")), Enum("Token", Enum("AUSD"))])` + * + * Input: `{ type: "LiquidCrowdloan", value: 13 }` + * Output: `Enum("LiquidCrowdloan", 13)` + * + * Input: `{ type: "NativeToken", value: "bigint:2" }` + * Output: `Enum("NativeToken", 2n)` + * + * Input: `{ type: "Erc20", value: "hex:0x07df96d1341a7d16ba1ad431e2c847d978bc2bce" }` + * Output: `Enum("Erc20", Binary.fromHex("0x07df96d1341a7d16ba1ad431e2c847d978bc2bce"))` + * + * Input: `{ type: "Stellar", value: { code: "bin:TZS", issuer: "hex:0x34c94b2a4ba9e8b57b22547dcbb30f443c4cb02da3829a89aa1bd4780e4466ba" } }` + * Output: `Enum("Stellar", { code: Binary.fromText("TZS"), issuer: Binary.fromHex("0x34c94b2a4ba9e8b57b22547dcbb30f443c4cb02da3829a89aa1bd4780e4466ba") })` + */ +export const papiParse = (text: string | T): T => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const reviver = (_key: string, value: any) => { + if (typeof value !== "string") return value + if (value.startsWith("bigint:")) return BigInt(value.slice("bigint:".length)) + if (value.startsWith("hex:")) return Binary.fromHex(value.slice("hex:".length)) + if (value.startsWith("bin:")) return Binary.fromText(value.slice("bin:".length)) + return value + } + + if (typeof text !== "string") return text + return JSON.parse(text, reviver) +} + +export const papiStringify = ( + value: any, // eslint-disable-line @typescript-eslint/no-explicit-any + space?: string | number +): string => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const replacer = (_key: string, value: any) => { + if (typeof value === "bigint") return `bigint:${String(value)}` + if (value instanceof Binary) return `hex:${value.asHex()}` + return value + } + + return JSON.stringify(value, replacer, space) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 115891e4c..b5654702a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1609,6 +1609,9 @@ importers: anylogger: specifier: ^1.0.11 version: 1.0.11 + polkadot-api: + specifier: ^0.6.0 + version: 0.6.0(@swc/core@1.3.14)(bufferutil@4.0.8)(jiti@1.21.0)(postcss@8.4.33)(rxjs@7.8.1)(smoldot@2.0.25(bufferutil@4.0.8)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) scale-ts: specifier: ^1.6.0 version: 1.6.0