@talismn/sapi 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ type RpcSendFunc = <T>(method: string, params: unknown[], isCacheable?: boolean) => Promise<T>;
2
+ /**
3
+ * Fetches the highest supported version of metadata from the chain.
4
+ *
5
+ * @param rpcSend
6
+ * @returns hex-encoded metadata starting with the magic number
7
+ */
8
+ export declare const fetchBestMetadata: (rpcSend: RpcSendFunc, allowLegacyFallback?: boolean) => Promise<`0x${string}`>;
9
+ export {};
@@ -1,2 +1,3 @@
1
1
  export * from "./types";
2
2
  export * from "./sapi";
3
+ export * from "./fetchBestMetadata";
@@ -9,6 +9,7 @@ var utils = require('@polkadot-api/utils');
9
9
  var types = require('@polkadot/types');
10
10
  var merkleizeMetadata = require('@polkadot-api/merkleize-metadata');
11
11
  var substrateBindings = require('@polkadot-api/substrate-bindings');
12
+ var scaleTs = require('scale-ts');
12
13
 
13
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
15
 
@@ -430,27 +431,26 @@ function trailingZeroes(n) {
430
431
  return i;
431
432
  }
432
433
 
434
+ const PERIOD = 64; // validity period in blocks, used for mortal era
435
+
433
436
  const getSignerPayloadJSON = async (chain, palletName, methodName, args, signerConfig, chainInfo) => {
434
437
  const {
435
438
  codec,
436
439
  location
437
440
  } = chain.builder.buildCall(palletName, methodName);
438
441
  const method = polkadotApi.Binary.fromBytes(utils.mergeUint8([new Uint8Array(location), codec.enc(args)]));
439
- const blockNumber = await getStorageValue(chain, "System", "Number", []);
440
- if (blockNumber === null) throw new Error("Block number not found");
441
- const [account, genesisHash, blockHash] = await Promise.all([
442
- // TODO if V15 available, use a runtime call instead : AccountNonceApi/account_nonce
443
- // about nonce https://github.com/paritytech/json-rpc-interface-spec/issues/156
444
- getStorageValue(chain, "System", "Account", [signerConfig.address]), getStorageValue(chain, "System", "BlockHash", [0]), getSendRequestResult(chain, "chain_getBlockHash", [blockNumber], false) // TODO find the right way to fetch this with new RPC api, this is not available in storage yet
445
- ]);
442
+
443
+ // on unstable networks with lots of forks (ex: westend asset hub as of june 2025),
444
+ // using a finalized block as reference for mortality is necessary for txs to get through
445
+ const blockHash = await getSendRequestResult(chain, "chain_getFinalizedHead", [], false);
446
+ const [nonce, genesisHash, blockNumber] = await Promise.all([getSendRequestResult(chain, "system_accountNextIndex", [signerConfig.address], false), getStorageValue(chain, "System", "BlockHash", [0]), getStorageValue(chain, "System", "Number", [], blockHash)]);
446
447
  if (!genesisHash) throw new Error("Genesis hash not found");
447
448
  if (!blockHash) throw new Error("Block hash not found");
448
- const nonce = account ? account.nonce : 0;
449
449
  const era = mortal({
450
- period: 16,
451
- phase: blockNumber % 16
450
+ period: PERIOD,
451
+ phase: blockNumber % PERIOD
452
452
  });
453
- const signedExtensions = chain.metadata.extrinsic.signedExtensions.map(ext => ext.identifier.toString());
453
+ const signedExtensions = chain.metadata.extrinsic.signedExtensions.map(ext => ext.identifier);
454
454
  const basePayload = {
455
455
  address: signerConfig.address,
456
456
  genesisHash: genesisHash.asHex(),
@@ -530,4 +530,51 @@ const getScaleApi = (connector, hexMetadata, token, hasCheckMetadataHash, signed
530
530
  };
531
531
  };
532
532
 
533
+ const MAGIC_NUMBER = 1635018093;
534
+
535
+ // it's important to set a max because some chains also return high invalid version numbers in the metadata_versions list (ex on Polkadot, related to JAM?)
536
+ const MAX_SUPPORTED_METADATA_VERSION = 16;
537
+ /**
538
+ * Fetches the highest supported version of metadata from the chain.
539
+ *
540
+ * @param rpcSend
541
+ * @returns hex-encoded metadata starting with the magic number
542
+ */
543
+ const fetchBestMetadata = async (rpcSend, allowLegacyFallback) => {
544
+ try {
545
+ // fetch available versions of metadata
546
+ const metadataVersions = await rpcSend("state_call", ["Metadata_metadata_versions", "0x"], true);
547
+ const availableVersions = scaleTs.Vector(scaleTs.u32).dec(metadataVersions);
548
+ const bestVersion = Math.max(...availableVersions.filter(v => v <= MAX_SUPPORTED_METADATA_VERSION));
549
+ const metadata = await rpcSend("state_call", ["Metadata_metadata_at_version", utils.toHex(scaleTs.u32.enc(bestVersion))], true);
550
+ return normalizeMetadata(metadata);
551
+ } catch (cause) {
552
+ // if the chain doesnt support the Metadata pallet, fallback to legacy rpc provided metadata (V14)
553
+ const message = cause?.message;
554
+ if (allowLegacyFallback || message?.includes("is not found") ||
555
+ // ex: crust standalone
556
+ message?.includes("Module doesn't have export Metadata_metadata_versions") // ex: 3DPass
557
+ ) {
558
+ return await rpcSend("state_getMetadata", [], true);
559
+ }
560
+
561
+ // otherwise throw so it can be handled by the caller
562
+ throw new Error("Failed to fetch metadata", {
563
+ cause
564
+ });
565
+ }
566
+ };
567
+
568
+ /**
569
+ * Removes everything before the magic number in the metadata.
570
+ * This ensures Opaque metadata is usable by pjs
571
+ */
572
+ const normalizeMetadata = metadata => {
573
+ const hexMagicNumber = utils.toHex(scaleTs.u32.enc(MAGIC_NUMBER)).slice(2);
574
+ const magicNumberIndex = metadata.indexOf(hexMagicNumber);
575
+ if (magicNumberIndex === -1) throw new Error("Invalid metadata format: magic number not found");
576
+ return `0x${metadata.slice(magicNumberIndex)}`;
577
+ };
578
+
579
+ exports.fetchBestMetadata = fetchBestMetadata;
533
580
  exports.getScaleApi = getScaleApi;
@@ -9,6 +9,7 @@ var utils = require('@polkadot-api/utils');
9
9
  var types = require('@polkadot/types');
10
10
  var merkleizeMetadata = require('@polkadot-api/merkleize-metadata');
11
11
  var substrateBindings = require('@polkadot-api/substrate-bindings');
12
+ var scaleTs = require('scale-ts');
12
13
 
13
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
15
 
@@ -430,27 +431,26 @@ function trailingZeroes(n) {
430
431
  return i;
431
432
  }
432
433
 
434
+ const PERIOD = 64; // validity period in blocks, used for mortal era
435
+
433
436
  const getSignerPayloadJSON = async (chain, palletName, methodName, args, signerConfig, chainInfo) => {
434
437
  const {
435
438
  codec,
436
439
  location
437
440
  } = chain.builder.buildCall(palletName, methodName);
438
441
  const method = polkadotApi.Binary.fromBytes(utils.mergeUint8([new Uint8Array(location), codec.enc(args)]));
439
- const blockNumber = await getStorageValue(chain, "System", "Number", []);
440
- if (blockNumber === null) throw new Error("Block number not found");
441
- const [account, genesisHash, blockHash] = await Promise.all([
442
- // TODO if V15 available, use a runtime call instead : AccountNonceApi/account_nonce
443
- // about nonce https://github.com/paritytech/json-rpc-interface-spec/issues/156
444
- getStorageValue(chain, "System", "Account", [signerConfig.address]), getStorageValue(chain, "System", "BlockHash", [0]), getSendRequestResult(chain, "chain_getBlockHash", [blockNumber], false) // TODO find the right way to fetch this with new RPC api, this is not available in storage yet
445
- ]);
442
+
443
+ // on unstable networks with lots of forks (ex: westend asset hub as of june 2025),
444
+ // using a finalized block as reference for mortality is necessary for txs to get through
445
+ const blockHash = await getSendRequestResult(chain, "chain_getFinalizedHead", [], false);
446
+ const [nonce, genesisHash, blockNumber] = await Promise.all([getSendRequestResult(chain, "system_accountNextIndex", [signerConfig.address], false), getStorageValue(chain, "System", "BlockHash", [0]), getStorageValue(chain, "System", "Number", [], blockHash)]);
446
447
  if (!genesisHash) throw new Error("Genesis hash not found");
447
448
  if (!blockHash) throw new Error("Block hash not found");
448
- const nonce = account ? account.nonce : 0;
449
449
  const era = mortal({
450
- period: 16,
451
- phase: blockNumber % 16
450
+ period: PERIOD,
451
+ phase: blockNumber % PERIOD
452
452
  });
453
- const signedExtensions = chain.metadata.extrinsic.signedExtensions.map(ext => ext.identifier.toString());
453
+ const signedExtensions = chain.metadata.extrinsic.signedExtensions.map(ext => ext.identifier);
454
454
  const basePayload = {
455
455
  address: signerConfig.address,
456
456
  genesisHash: genesisHash.asHex(),
@@ -530,4 +530,51 @@ const getScaleApi = (connector, hexMetadata, token, hasCheckMetadataHash, signed
530
530
  };
531
531
  };
532
532
 
533
+ const MAGIC_NUMBER = 1635018093;
534
+
535
+ // it's important to set a max because some chains also return high invalid version numbers in the metadata_versions list (ex on Polkadot, related to JAM?)
536
+ const MAX_SUPPORTED_METADATA_VERSION = 16;
537
+ /**
538
+ * Fetches the highest supported version of metadata from the chain.
539
+ *
540
+ * @param rpcSend
541
+ * @returns hex-encoded metadata starting with the magic number
542
+ */
543
+ const fetchBestMetadata = async (rpcSend, allowLegacyFallback) => {
544
+ try {
545
+ // fetch available versions of metadata
546
+ const metadataVersions = await rpcSend("state_call", ["Metadata_metadata_versions", "0x"], true);
547
+ const availableVersions = scaleTs.Vector(scaleTs.u32).dec(metadataVersions);
548
+ const bestVersion = Math.max(...availableVersions.filter(v => v <= MAX_SUPPORTED_METADATA_VERSION));
549
+ const metadata = await rpcSend("state_call", ["Metadata_metadata_at_version", utils.toHex(scaleTs.u32.enc(bestVersion))], true);
550
+ return normalizeMetadata(metadata);
551
+ } catch (cause) {
552
+ // if the chain doesnt support the Metadata pallet, fallback to legacy rpc provided metadata (V14)
553
+ const message = cause?.message;
554
+ if (allowLegacyFallback || message?.includes("is not found") ||
555
+ // ex: crust standalone
556
+ message?.includes("Module doesn't have export Metadata_metadata_versions") // ex: 3DPass
557
+ ) {
558
+ return await rpcSend("state_getMetadata", [], true);
559
+ }
560
+
561
+ // otherwise throw so it can be handled by the caller
562
+ throw new Error("Failed to fetch metadata", {
563
+ cause
564
+ });
565
+ }
566
+ };
567
+
568
+ /**
569
+ * Removes everything before the magic number in the metadata.
570
+ * This ensures Opaque metadata is usable by pjs
571
+ */
572
+ const normalizeMetadata = metadata => {
573
+ const hexMagicNumber = utils.toHex(scaleTs.u32.enc(MAGIC_NUMBER)).slice(2);
574
+ const magicNumberIndex = metadata.indexOf(hexMagicNumber);
575
+ if (magicNumberIndex === -1) throw new Error("Invalid metadata format: magic number not found");
576
+ return `0x${metadata.slice(magicNumberIndex)}`;
577
+ };
578
+
579
+ exports.fetchBestMetadata = fetchBestMetadata;
533
580
  exports.getScaleApi = getScaleApi;
@@ -7,6 +7,7 @@ import { toHex, mergeUint8 } from '@polkadot-api/utils';
7
7
  import { TypeRegistry, Metadata } from '@polkadot/types';
8
8
  import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata';
9
9
  import { enhanceEncoder, Bytes, u16 } from '@polkadot-api/substrate-bindings';
10
+ import { Vector, u32 } from 'scale-ts';
10
11
 
11
12
  var packageJson = {
12
13
  name: "@talismn/sapi"};
@@ -424,27 +425,26 @@ function trailingZeroes(n) {
424
425
  return i;
425
426
  }
426
427
 
428
+ const PERIOD = 64; // validity period in blocks, used for mortal era
429
+
427
430
  const getSignerPayloadJSON = async (chain, palletName, methodName, args, signerConfig, chainInfo) => {
428
431
  const {
429
432
  codec,
430
433
  location
431
434
  } = chain.builder.buildCall(palletName, methodName);
432
435
  const method = Binary.fromBytes(mergeUint8([new Uint8Array(location), codec.enc(args)]));
433
- const blockNumber = await getStorageValue(chain, "System", "Number", []);
434
- if (blockNumber === null) throw new Error("Block number not found");
435
- const [account, genesisHash, blockHash] = await Promise.all([
436
- // TODO if V15 available, use a runtime call instead : AccountNonceApi/account_nonce
437
- // about nonce https://github.com/paritytech/json-rpc-interface-spec/issues/156
438
- getStorageValue(chain, "System", "Account", [signerConfig.address]), getStorageValue(chain, "System", "BlockHash", [0]), getSendRequestResult(chain, "chain_getBlockHash", [blockNumber], false) // TODO find the right way to fetch this with new RPC api, this is not available in storage yet
439
- ]);
436
+
437
+ // on unstable networks with lots of forks (ex: westend asset hub as of june 2025),
438
+ // using a finalized block as reference for mortality is necessary for txs to get through
439
+ const blockHash = await getSendRequestResult(chain, "chain_getFinalizedHead", [], false);
440
+ const [nonce, genesisHash, blockNumber] = await Promise.all([getSendRequestResult(chain, "system_accountNextIndex", [signerConfig.address], false), getStorageValue(chain, "System", "BlockHash", [0]), getStorageValue(chain, "System", "Number", [], blockHash)]);
440
441
  if (!genesisHash) throw new Error("Genesis hash not found");
441
442
  if (!blockHash) throw new Error("Block hash not found");
442
- const nonce = account ? account.nonce : 0;
443
443
  const era = mortal({
444
- period: 16,
445
- phase: blockNumber % 16
444
+ period: PERIOD,
445
+ phase: blockNumber % PERIOD
446
446
  });
447
- const signedExtensions = chain.metadata.extrinsic.signedExtensions.map(ext => ext.identifier.toString());
447
+ const signedExtensions = chain.metadata.extrinsic.signedExtensions.map(ext => ext.identifier);
448
448
  const basePayload = {
449
449
  address: signerConfig.address,
450
450
  genesisHash: genesisHash.asHex(),
@@ -524,4 +524,50 @@ const getScaleApi = (connector, hexMetadata, token, hasCheckMetadataHash, signed
524
524
  };
525
525
  };
526
526
 
527
- export { getScaleApi };
527
+ const MAGIC_NUMBER = 1635018093;
528
+
529
+ // it's important to set a max because some chains also return high invalid version numbers in the metadata_versions list (ex on Polkadot, related to JAM?)
530
+ const MAX_SUPPORTED_METADATA_VERSION = 16;
531
+ /**
532
+ * Fetches the highest supported version of metadata from the chain.
533
+ *
534
+ * @param rpcSend
535
+ * @returns hex-encoded metadata starting with the magic number
536
+ */
537
+ const fetchBestMetadata = async (rpcSend, allowLegacyFallback) => {
538
+ try {
539
+ // fetch available versions of metadata
540
+ const metadataVersions = await rpcSend("state_call", ["Metadata_metadata_versions", "0x"], true);
541
+ const availableVersions = Vector(u32).dec(metadataVersions);
542
+ const bestVersion = Math.max(...availableVersions.filter(v => v <= MAX_SUPPORTED_METADATA_VERSION));
543
+ const metadata = await rpcSend("state_call", ["Metadata_metadata_at_version", toHex(u32.enc(bestVersion))], true);
544
+ return normalizeMetadata(metadata);
545
+ } catch (cause) {
546
+ // if the chain doesnt support the Metadata pallet, fallback to legacy rpc provided metadata (V14)
547
+ const message = cause?.message;
548
+ if (allowLegacyFallback || message?.includes("is not found") ||
549
+ // ex: crust standalone
550
+ message?.includes("Module doesn't have export Metadata_metadata_versions") // ex: 3DPass
551
+ ) {
552
+ return await rpcSend("state_getMetadata", [], true);
553
+ }
554
+
555
+ // otherwise throw so it can be handled by the caller
556
+ throw new Error("Failed to fetch metadata", {
557
+ cause
558
+ });
559
+ }
560
+ };
561
+
562
+ /**
563
+ * Removes everything before the magic number in the metadata.
564
+ * This ensures Opaque metadata is usable by pjs
565
+ */
566
+ const normalizeMetadata = metadata => {
567
+ const hexMagicNumber = toHex(u32.enc(MAGIC_NUMBER)).slice(2);
568
+ const magicNumberIndex = metadata.indexOf(hexMagicNumber);
569
+ if (magicNumberIndex === -1) throw new Error("Invalid metadata format: magic number not found");
570
+ return `0x${metadata.slice(magicNumberIndex)}`;
571
+ };
572
+
573
+ export { fetchBestMetadata, getScaleApi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talismn/sapi",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -30,6 +30,7 @@
30
30
  "@polkadot/util": "13.5.1",
31
31
  "anylogger": "^1.0.11",
32
32
  "polkadot-api": "1.13.1",
33
+ "scale-ts": "^1.6.1",
33
34
  "@talismn/scale": "0.1.2"
34
35
  },
35
36
  "devDependencies": {