From 40b342540eaf287787dca717d428d6eb5b0c2e0a Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Fri, 6 Mar 2026 15:06:53 -0800 Subject: [PATCH 1/3] feat: add WASM-based DOT transaction parsing via @bitgo/wasm-dot Integrates @bitgo/wasm-dot into sdk-coin-dot for WASM-based transaction parsing using Rust/subxt, replacing the JS txwrapper-polkadot path for tdot (testnet) signed transactions. ## what changed - **wasmParser.ts**: new module with `explainDotTransaction()` and `toJsonFromWasm()` that parse DOT extrinsics via WASM and map to BitGoJS TransactionExplanation / TxData formats. handles transfers, staking (bond, unbond, withdraw, chill, payout), proxy (add/remove), batch (nested + atomic), and transferAll. - **transaction.ts toJson()**: signed tdot transactions now use the WASM path (`toJsonFromWasm`), which handles metadata-aware signed extension parsing (e.g. Westend's AuthorizeCall, StorageWeightReclaim). unsigned transactions still use the legacy txwrapper path. - **webpack config**: added ESM alias for @bitgo/wasm-dot so browser builds use the fetch-based WASM loader instead of fs.readFileSync. - **tests**: 72+ new test lines in dot.ts covering WASM explain for transfers and staking. 511-line byte comparison test suite validating WASM builder output matches legacy txwrapper output byte-for-byte across all transaction types (transfer, stake, unstake, withdraw, chill, proxy, batch). - **withdrawUnstakedBuilder test fix**: numSlashingSpans assertion updated from string to number to match actual type. ## scope WASM path is only active for tdot signed transactions, gated by `this._coinConfig.name === 'tdot' && this._signedTransaction`. mainnet dot remains on the legacy path until validation is complete. BTC-0 TICKET: BTC-0 --- modules/sdk-coin-dot/package.json | 1 + modules/sdk-coin-dot/src/dot.ts | 8 - modules/sdk-coin-dot/src/lib/index.ts | 2 + modules/sdk-coin-dot/src/lib/transaction.ts | 15 + modules/sdk-coin-dot/src/lib/wasmParser.ts | 389 +++++++++++++ modules/sdk-coin-dot/test/unit/dot.ts | 72 ++- .../withdrawUnstakedBuilder.ts | 6 +- .../test/unit/wasmBuilderByteComparison.ts | 511 ++++++++++++++++++ webpack/bitgojs.config.js | 1 + yarn.lock | 5 + 10 files changed, 997 insertions(+), 13 deletions(-) create mode 100644 modules/sdk-coin-dot/src/lib/wasmParser.ts create mode 100644 modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts diff --git a/modules/sdk-coin-dot/package.json b/modules/sdk-coin-dot/package.json index 38a7625e66..854e67fe7b 100644 --- a/modules/sdk-coin-dot/package.json +++ b/modules/sdk-coin-dot/package.json @@ -43,6 +43,7 @@ "@bitgo/sdk-core": "^36.33.2", "@bitgo/sdk-lib-mpc": "^10.9.0", "@bitgo/statics": "^58.29.0", + "@bitgo/wasm-dot": "^1.1.2", "@polkadot/api": "14.1.1", "@polkadot/api-augment": "14.1.1", "@polkadot/keyring": "13.5.6", diff --git a/modules/sdk-coin-dot/src/dot.ts b/modules/sdk-coin-dot/src/dot.ts index 844ea1630d..f45f63e4a5 100644 --- a/modules/sdk-coin-dot/src/dot.ts +++ b/modules/sdk-coin-dot/src/dot.ts @@ -58,14 +58,6 @@ export interface TransactionPrebuild { transaction: Interface.TxData; } -export interface ExplainTransactionOptions { - txPrebuild: TransactionPrebuild; - publicKey: string; - feeInfo: { - fee: string; - }; -} - export interface VerifiedTransactionParameters { txHex: string; prv: string; diff --git a/modules/sdk-coin-dot/src/lib/index.ts b/modules/sdk-coin-dot/src/lib/index.ts index c7aefdc345..d02b846001 100644 --- a/modules/sdk-coin-dot/src/lib/index.ts +++ b/modules/sdk-coin-dot/src/lib/index.ts @@ -16,4 +16,6 @@ export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { SingletonRegistry } from './singletonRegistry'; export { NativeTransferBuilder } from './nativeTransferBuilder'; export { RemoveProxyBuilder } from './proxyBuilder'; +export { explainDotTransaction } from './wasmParser'; +export type { ExplainDotTransactionParams, DotWasmExplanation, DotInput } from './wasmParser'; export { Interface, Utils }; diff --git a/modules/sdk-coin-dot/src/lib/transaction.ts b/modules/sdk-coin-dot/src/lib/transaction.ts index 153d0d307a..5e0e6c9b53 100644 --- a/modules/sdk-coin-dot/src/lib/transaction.ts +++ b/modules/sdk-coin-dot/src/lib/transaction.ts @@ -38,6 +38,7 @@ import { } from './iface'; import { getAddress, getDelegateAddress } from './iface_utils'; import utils from './utils'; +import { toJsonFromWasm } from './wasmParser'; import BigNumber from 'bignumber.js'; import { Vec } from '@polkadot/types'; import { PalletConstantMetadataV14 } from '@polkadot/types/interfaces'; @@ -161,6 +162,20 @@ export class Transaction extends BaseTransaction { if (!this._dotTransaction) { throw new InvalidTransactionError('Empty transaction'); } + + // WASM path for signed tdot transactions — validates WASM parsing against production. + // Only for signed txs because toBroadcastFormat() returns the signed extrinsic (parseable). + // Unsigned txs return a signing payload (different format), so they use the legacy path. + if (this._coinConfig.name === 'tdot' && this._signedTransaction) { + return toJsonFromWasm({ + txHex: this._signedTransaction, + material: utils.getMaterial(this._coinConfig), + senderAddress: this._sender, + coinConfigName: this._coinConfig.name, + referenceBlock: this._dotTransaction.blockHash, + blockNumber: Number(this._dotTransaction.blockNumber), + }); + } const decodedTx = decode(this._dotTransaction, { metadataRpc: this._dotTransaction.metadataRpc, registry: this._registry, diff --git a/modules/sdk-coin-dot/src/lib/wasmParser.ts b/modules/sdk-coin-dot/src/lib/wasmParser.ts new file mode 100644 index 0000000000..3cc289f5b7 --- /dev/null +++ b/modules/sdk-coin-dot/src/lib/wasmParser.ts @@ -0,0 +1,389 @@ +/** + * WASM-based DOT transaction explanation. + * + * Built on @bitgo/wasm-dot's parseTransaction(). Derives transaction types, + * extracts outputs/inputs, and maps to BitGoJS TransactionExplanation format. + * This is BitGo-specific business logic that lives outside the wasm package. + */ + +import { TransactionType } from '@bitgo/sdk-core'; +import { DotTransaction, parseTransaction, type ParsedMethod, type Era } from '@bitgo/wasm-dot'; +import type { BatchCallObject, ProxyType, TransactionExplanation, Material, TxData } from './iface'; + +const MAX_NESTING_DEPTH = 10; + +// ============================================================================= +// Public types +// ============================================================================= + +/** + * Input entry for a DOT transaction. + * For account-model chains, there's typically one input (the sender). + */ +export interface DotInput { + address: string; + value: number; + valueString: string; +} + +/** + * Extended explanation returned by WASM-based parsing. + * Includes fields needed by wallet-platform that aren't in the base TransactionExplanation. + */ +export interface DotWasmExplanation extends TransactionExplanation { + sender: string; + nonce: number; + isSigned: boolean; + methodName: string; + inputs: DotInput[]; +} + +export interface ExplainDotTransactionParams { + txHex: string; + material: Material; + senderAddress?: string; +} + +export interface ToJsonFromWasmParams { + txHex: string; + material: Material; + senderAddress: string; + coinConfigName: string; + referenceBlock?: string; + blockNumber?: number; +} + +// ============================================================================= +// Main exports +// ============================================================================= + +/** + * Explain a DOT transaction using the WASM parser. + * + * Parses the transaction via WASM, derives the transaction type and + * outputs locally, then maps to BitGoJS TransactionExplanation format. + */ +export function explainDotTransaction(params: ExplainDotTransactionParams): DotWasmExplanation { + const explained = buildExplanation(params); + + const sender = explained.sender || params.senderAddress || ''; + const type = mapTransactionType(explained.typeName); + const methodName = `${explained.method.pallet}.${explained.method.name}`; + + const outputs = explained.outputs.map((o) => ({ + address: o.address, + amount: o.amount === 'ALL' ? '0' : o.amount, + })); + + const inputs: DotInput[] = explained.inputs.map((i) => { + const value = i.value === 'ALL' ? 0 : parseInt(i.value || '0', 10); + return { address: i.address, value, valueString: i.value }; + }); + + return { + displayOrder: ['outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'type', 'sequenceId', 'id'], + id: explained.id || '', + outputs, + outputAmount: explained.outputAmount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: explained.tip || '0', type: 'tip' }, + type, + sender, + nonce: explained.nonce, + isSigned: explained.isSigned, + methodName, + inputs, + }; +} + +/** + * Produce a TxData object using WASM parsing instead of the JS txwrapper. + * + * This replaces the legacy `toJson()` path for chains where WASM parsing is enabled. + * The WASM parser decodes the extrinsic bytes (with metadata-aware signed extension handling), + * and this function maps the result to the TxData interface that consumers expect. + */ +export function toJsonFromWasm(params: ToJsonFromWasmParams): TxData { + const explained = buildExplanation(params); + const type = mapTransactionType(explained.typeName); + const method = explained.method; + const args = method.args as Record; + + const result: TxData = { + id: explained.id || '', + sender: explained.sender || params.senderAddress, + referenceBlock: params.referenceBlock || '', + blockNumber: params.blockNumber || 0, + genesisHash: params.material.genesisHash || '', + nonce: explained.nonce, + specVersion: params.material.specVersion || 0, + transactionVersion: params.material.txVersion || 0, + chainName: params.material.chainName || '', + tip: Number(explained.tip) || 0, + eraPeriod: explained.era.type === 'mortal' ? (explained.era as { period: number }).period : 0, + }; + + if (type === TransactionType.Send) { + populateSendFields(result, method, args); + } else if (type === TransactionType.StakingActivate) { + populateStakingActivateFields(result, method, args, params.senderAddress); + } else if (type === TransactionType.StakingUnlock) { + result.amount = String(args.value ?? ''); + } else if (type === TransactionType.StakingWithdraw) { + result.numSlashingSpans = Number(args.numSlashingSpans ?? 0); + } else if (type === TransactionType.StakingClaim) { + result.validatorStash = String(args.validatorStash ?? ''); + result.claimEra = String(args.era ?? ''); + } else if (type === TransactionType.AddressInitialization) { + populateAddressInitFields(result, method, args); + } else if (type === TransactionType.Batch) { + result.batchCalls = mapBatchCalls(args.calls as ParsedMethod[]); + } + + return result; +} + +// ============================================================================= +// Core explain logic (moved from @bitgo/wasm-dot) +// ============================================================================= + +interface InternalExplained { + typeName: string; + id: string | undefined; + sender: string | undefined; + outputs: { address: string; amount: string }[]; + inputs: { address: string; value: string }[]; + outputAmount: string; + tip: string; + era: Era; + method: ParsedMethod; + isSigned: boolean; + nonce: number; +} + +/** Parse and explain a DOT transaction. Replaces the old wasm-dot explainTransaction. */ +function buildExplanation(params: { + txHex: string; + material: Material; + senderAddress?: string; + referenceBlock?: string; + blockNumber?: number; +}): InternalExplained { + const tx = DotTransaction.fromHex(params.txHex, params.material); + const context = { + material: params.material, + sender: params.senderAddress, + referenceBlock: params.referenceBlock, + blockNumber: params.blockNumber, + }; + const parsed = parseTransaction(tx, context); + + const typeName = deriveTransactionType(parsed.method, 0); + const outputs = extractOutputs(parsed.method, 0); + const sender = parsed.sender ?? params.senderAddress; + const inputs: { address: string; value: string }[] = sender + ? outputs.map((o) => ({ address: sender, value: o.amount })) + : []; + + const outputAmount = outputs.reduce((sum, o) => { + if (o.amount === 'ALL') return sum; + return (BigInt(sum) + BigInt(o.amount)).toString(); + }, '0'); + + return { + typeName, + id: parsed.id ?? undefined, + sender: parsed.sender ?? undefined, + outputs, + inputs, + outputAmount, + tip: parsed.tip, + era: parsed.era, + method: parsed.method, + isSigned: parsed.isSigned, + nonce: parsed.nonce, + }; +} + +// ============================================================================= +// Transaction type derivation (moved from @bitgo/wasm-dot) +// ============================================================================= + +function deriveTransactionType(method: ParsedMethod, depth: number): string { + const key = `${method.pallet}.${method.name}`; + const args = (method.args ?? {}) as Record; + switch (key) { + case 'balances.transfer': + case 'balances.transferKeepAlive': + case 'balances.transferAllowDeath': + case 'balances.transferAll': + return 'Send'; + + case 'staking.bond': + case 'staking.bondExtra': + return 'StakingActivate'; + + case 'staking.unbond': + return 'StakingUnlock'; + + case 'staking.withdrawUnbonded': + return 'StakingWithdraw'; + + case 'staking.chill': + return 'StakingUnvote'; + + case 'staking.payoutStakers': + return 'StakingClaim'; + + case 'proxy.addProxy': + case 'proxy.removeProxy': + case 'proxy.createPure': + return 'AddressInitialization'; + + case 'utility.batch': + case 'utility.batchAll': + return 'Batch'; + + case 'proxy.proxy': { + if (depth >= MAX_NESTING_DEPTH) return 'Unknown'; + const call = args.call as ParsedMethod | undefined; + if (call?.pallet && call?.name) return deriveTransactionType(call, depth + 1); + return 'Unknown'; + } + + default: + return 'Unknown'; + } +} + +// ============================================================================= +// Output extraction (moved from @bitgo/wasm-dot) +// ============================================================================= + +function extractOutputs(method: ParsedMethod, depth: number): { address: string; amount: string }[] { + const args = (method.args ?? {}) as Record; + const key = `${method.pallet}.${method.name}`; + + switch (key) { + case 'balances.transfer': + case 'balances.transferKeepAlive': + case 'balances.transferAllowDeath': + return [{ address: String(args.dest ?? ''), amount: String(args.value ?? '0') }]; + + case 'balances.transferAll': + return [{ address: String(args.dest ?? ''), amount: 'ALL' }]; + + case 'staking.bond': + case 'staking.bondExtra': + case 'staking.unbond': + return [{ address: 'STAKING', amount: String(args.value ?? '0') }]; + + case 'utility.batch': + case 'utility.batchAll': { + if (depth >= MAX_NESTING_DEPTH) return []; + const calls = (args.calls ?? []) as ParsedMethod[]; + return calls.filter((c) => c?.pallet && c?.name).flatMap((c) => extractOutputs(c, depth + 1)); + } + + case 'proxy.proxy': { + if (depth >= MAX_NESTING_DEPTH) return []; + const call = args.call as ParsedMethod | undefined; + return call?.pallet && call?.name ? extractOutputs(call, depth + 1) : []; + } + + default: + return []; + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +/** Map type name string to sdk-core TransactionType via name lookup */ +function mapTransactionType(typeName: string): TransactionType { + return TransactionType[typeName as keyof typeof TransactionType] ?? TransactionType.Send; +} + +function populateSendFields(result: TxData, method: ParsedMethod, args: Record): void { + const key = `${method.pallet}.${method.name}`; + + if (key === 'proxy.proxy') { + // Proxy-wrapped transfer + const innerCall = args.call as ParsedMethod | undefined; + result.owner = String(args.real ?? ''); + result.forceProxyType = (args.forceProxyType as ProxyType) ?? undefined; + if (innerCall?.args) { + const innerArgs = innerCall.args as Record; + result.to = String(innerArgs.dest ?? ''); + result.amount = String(innerArgs.value ?? ''); + } + } else if (key === 'balances.transferAll') { + result.to = String(args.dest ?? ''); + result.keepAlive = Boolean(args.keepAlive); + } else { + // transfer, transferKeepAlive, transferAllowDeath + result.to = String(args.dest ?? ''); + result.amount = String(args.value ?? ''); + } +} + +function populateStakingActivateFields( + result: TxData, + method: ParsedMethod, + args: Record, + senderAddress: string +): void { + if (method.name === 'bondExtra') { + result.amount = String(args.value ?? ''); + } else { + // bond + result.controller = senderAddress; + result.amount = String(args.value ?? ''); + result.payee = String(args.payee ?? ''); + } +} + +function populateAddressInitFields(result: TxData, method: ParsedMethod, args: Record): void { + const key = `${method.pallet}.${method.name}`; + result.method = key; + result.proxyType = String(args.proxy_type ?? ''); + result.delay = String(args.delay ?? ''); + + if (key === 'proxy.createPure') { + result.index = String(args.index ?? ''); + } else { + // addProxy, removeProxy + result.owner = String(args.delegate ?? ''); + } +} + +function mapBatchCalls(calls: ParsedMethod[] | undefined): BatchCallObject[] { + if (!calls) return []; + return calls.map((call) => ({ + callIndex: `0x${call.palletIndex.toString(16).padStart(2, '0')}${call.methodIndex.toString(16).padStart(2, '0')}`, + args: transformBatchCallArgs((call.args ?? {}) as Record), + })); +} + +/** Transform WASM-decoded batch call args to match the Polkadot.js format that consumers expect */ +function transformBatchCallArgs(args: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(args)) { + if (key === 'delegate' && typeof value === 'string') { + // MultiAddress Id variant: string → { id: string } + result[key] = { id: value }; + } else if (key === 'value' && typeof value === 'string') { + // Compact u128: string → number (matches Polkadot.js behavior) + result[key] = Number(value); + } else if (key === 'payee' && typeof value === 'string') { + // Enum unit variant: "Staked" → { staked: null } + const variantName = value.charAt(0).toLowerCase() + value.slice(1); + result[key] = { [variantName]: null }; + } else { + result[key] = value; + } + } + return result; +} diff --git a/modules/sdk-coin-dot/test/unit/dot.ts b/modules/sdk-coin-dot/test/unit/dot.ts index 6f42c2a4d5..004f2807d4 100644 --- a/modules/sdk-coin-dot/test/unit/dot.ts +++ b/modules/sdk-coin-dot/test/unit/dot.ts @@ -7,7 +7,11 @@ import { Dot, Tdot, KeyPair } from '../../src'; import * as testData from '../fixtures'; import { chainName, txVersion, genesisHash, specVersion } from '../resources'; import * as sinon from 'sinon'; -import { Wallet } from '@bitgo/sdk-core'; +import { TransactionType, Wallet } from '@bitgo/sdk-core'; +import { coins } from '@bitgo/statics'; +import { buildTransaction, type BuildContext, type Material } from '@bitgo/wasm-dot'; +import utils from '../../src/lib/utils'; +import { explainDotTransaction } from '../../src/lib'; describe('DOT:', function () { let bitgo: TestBitGoAPI; @@ -152,7 +156,7 @@ describe('DOT:', function () { describe('Explain Transactions:', () => { it('should explain an unsigned transfer transaction', async function () { - const explainedTransaction = await basecoin.explainTransaction(testData.unsignedTransaction); + const explainedTransaction = await prodCoin.explainTransaction(testData.unsignedTransaction); explainedTransaction.should.deepEqual({ displayOrder: [ 'outputAmount', @@ -185,6 +189,70 @@ describe('DOT:', function () { }); }); + describe('Explain Transactions (WASM):', () => { + const coin = coins.get('tdot'); + const material = utils.getMaterial(coin); + const SENDER = testData.accounts.account1.address; + const RECIPIENT = testData.accounts.account2.address; + + function wasmContext(nonce = 0): BuildContext { + return { + sender: SENDER, + nonce, + material: material as Material, + validity: { firstValid: testData.westendBlock.blockNumber, maxDuration: 2400 }, + referenceBlock: testData.westendBlock.hash, + }; + } + + it('should explain a transfer via explainDotTransaction', function () { + const tx = buildTransaction({ type: 'transfer', to: RECIPIENT, amount: 1000000000000n }, wasmContext()); + const explained = explainDotTransaction({ + txHex: tx.toBroadcastFormat(), + material, + senderAddress: SENDER, + }); + + assert.strictEqual(explained.type, TransactionType.Send); + assert.strictEqual(explained.outputs.length, 1); + assert.strictEqual(explained.outputs[0].address, RECIPIENT); + assert.strictEqual(explained.outputs[0].amount, '1000000000000'); + assert.strictEqual(explained.outputAmount, '1000000000000'); + assert.strictEqual(explained.sender, SENDER); + assert.strictEqual(explained.methodName, 'balances.transferKeepAlive'); + }); + + it('should explain a staking bond via explainDotTransaction', function () { + const tx = buildTransaction({ type: 'stake', amount: 5000000000000n, payee: { type: 'stash' } }, wasmContext(1)); + const explained = explainDotTransaction({ + txHex: tx.toBroadcastFormat(), + material, + senderAddress: SENDER, + }); + + assert.strictEqual(explained.type, TransactionType.StakingActivate); + assert.strictEqual(explained.outputs.length, 1); + assert.strictEqual(explained.outputs[0].address, 'STAKING'); + assert.strictEqual(explained.outputs[0].amount, '5000000000000'); + }); + + it('should explain a transfer via explainDotTransaction with sender', function () { + const tx = buildTransaction({ type: 'transfer', to: RECIPIENT, amount: 1000000000000n }, wasmContext()); + const explained = explainDotTransaction({ + txHex: tx.toBroadcastFormat(), + material, + senderAddress: SENDER, + }); + + assert.strictEqual(explained.type, TransactionType.Send); + assert.strictEqual(explained.outputs.length, 1); + assert.strictEqual(explained.outputs[0].address, RECIPIENT); + assert.strictEqual(explained.outputs[0].amount, '1000000000000'); + assert.strictEqual(explained.sender, SENDER); + assert.strictEqual(explained.nonce, 0); + }); + }); + describe('Recover Transactions:', () => { const sandBox = sinon.createSandbox(); const destAddr = testData.accounts.account1.address; diff --git a/modules/sdk-coin-dot/test/unit/transactionBuilder/withdrawUnstakedBuilder.ts b/modules/sdk-coin-dot/test/unit/transactionBuilder/withdrawUnstakedBuilder.ts index 5fd131bf0f..f315167015 100644 --- a/modules/sdk-coin-dot/test/unit/transactionBuilder/withdrawUnstakedBuilder.ts +++ b/modules/sdk-coin-dot/test/unit/transactionBuilder/withdrawUnstakedBuilder.ts @@ -41,7 +41,7 @@ describe('Dot WithdrawUnstaked Builder', () => { builder.addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex')); const tx = await builder.build(); const txJson = tx.toJson(); - should.deepEqual(txJson.numSlashingSpans, '0'); + should.deepEqual(txJson.numSlashingSpans, 0); should.deepEqual(txJson.sender, sender.address); should.deepEqual(txJson.blockNumber, 3933); should.deepEqual(txJson.referenceBlock, refBlock); @@ -82,7 +82,7 @@ describe('Dot WithdrawUnstaked Builder', () => { builder.validity({ firstValid: 3933 }).referenceBlock(refBlock); const tx = await builder.build(); const txJson = tx.toJson(); - should.deepEqual(txJson.numSlashingSpans, '0'); + should.deepEqual(txJson.numSlashingSpans, 0); should.deepEqual(txJson.sender, sender.address); should.deepEqual(txJson.blockNumber, 3933); should.deepEqual(txJson.referenceBlock, refBlock); @@ -104,7 +104,7 @@ describe('Dot WithdrawUnstaked Builder', () => { .addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex')); const tx = await builder.build(); const txJson = tx.toJson(); - should.deepEqual(txJson.numSlashingSpans, '0'); + should.deepEqual(txJson.numSlashingSpans, 0); should.deepEqual(txJson.sender, sender.address); should.deepEqual(txJson.blockNumber, 3933); should.deepEqual(txJson.referenceBlock, refBlock); diff --git a/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts b/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts new file mode 100644 index 0000000000..c710a6959f --- /dev/null +++ b/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts @@ -0,0 +1,511 @@ +/** + * WASM Builder Byte Comparison Tests + * + * Compare serialized output between: + * 1. Legacy approach (using @substrate/txwrapper-polkadot) + * 2. WASM approach (using @bitgo/wasm-dot DotBuilder) + * + * For unsigned transactions, legacy toBroadcastFormat() returns the signing payload + * (via construct.signingPayload). We compare WASM signablePayload() against it. + * + * Format difference: txwrapper encodes the call as `Bytes` (Vec) which includes + * a SCALE compact-length prefix. subxt encodes it as raw `Call` (no prefix). + * We strip this prefix from the legacy side before comparing, since the actual + * call data, era, nonce, tip, and chain context are identical. + */ + +import assert from 'assert'; +import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; +import { TransferBuilder } from '../../src/lib/transferBuilder'; +import { ProxyType } from '../../src/lib/iface'; +import { accounts, westendBlock } from '../fixtures'; +import utils from '../../src/lib/utils'; + +// Import WASM builder +import { buildTransaction, type BuildContext, type Material } from '@bitgo/wasm-dot'; + +describe('WASM vs Legacy Builder Byte Comparison', function () { + const coin = coins.get('tdot'); + + // Get material from utils to ensure same metadata as legacy builder + const material = utils.getMaterial(coin); + + function createWasmContext(overrides: Partial = {}): BuildContext { + return { + sender: accounts.account1.address, + nonce: 0, + tip: 0n, + material: material as Material, + validity: { + firstValid: westendBlock.blockNumber, + maxDuration: 2400, + }, + referenceBlock: westendBlock.hash, + ...overrides, + }; + } + + /** + * Strip SCALE compact-length prefix from the legacy signing payload. + * + * Legacy (txwrapper) encodes the call as `Bytes` type, which adds a compact-length + * prefix. subxt encodes it as raw `Call` (no prefix). Both produce identical + * call data + era + nonce + tip + chain context after this prefix. + */ + function stripCompactPrefix(hex: string): string { + const data = hex.startsWith('0x') ? hex.slice(2) : hex; + const bytes = Buffer.from(data, 'hex'); + const mode = bytes[0] & 0b11; + let offset: number; + if (mode === 0b00) offset = 1; + else if (mode === 0b01) offset = 2; + else if (mode === 0b10) offset = 4; + else throw new Error('Big compact not supported'); + return '0x' + bytes.slice(offset).toString('hex'); + } + + // =========================================================================== + // Transfer Transaction Tests + // =========================================================================== + describe('Transfer Transactions', function () { + it('should produce identical signing payload for transfer', async function () { + const to = accounts.account2.address; + const amount = '1000000000000'; // 1 DOT + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getTransferBuilder() as TransferBuilder; + + legacyBuilder + .sender({ address: accounts.account1.address }) + .to({ address: to }) + .amount(amount) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { type: 'transfer', to, amount: BigInt(amount), keepAlive: true }, + createWasmContext() + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for transferKeepAlive with different nonce', async function () { + const to = accounts.account2.address; + const amount = '5000000000000'; // 5 DOT + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getTransferBuilder() as TransferBuilder; + + legacyBuilder + .sender({ address: accounts.account1.address }) + .to({ address: to }) + .amount(amount) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 5 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { type: 'transfer', to, amount: BigInt(amount), keepAlive: true }, + createWasmContext({ nonce: 5 }) + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + }); + + // =========================================================================== + // Staking Transaction Tests + // =========================================================================== + describe('Staking Transactions', function () { + it('should produce identical signing payload for staking bond', async function () { + const amount = '10000000000000'; // 10 DOT + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getStakingBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .amount(amount) + .payee('Staked') + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { type: 'stake', amount: BigInt(amount), payee: { type: 'staked' } }, + createWasmContext() + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for staking bond with Stash payee', async function () { + const amount = '20000000000000'; // 20 DOT + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getStakingBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .amount(amount) + .payee('Stash') + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { type: 'stake', amount: BigInt(amount), payee: { type: 'stash' } }, + createWasmContext() + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for unstake (unbond)', async function () { + const amount = '5000000000000'; // 5 DOT + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getUnstakeBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .amount(amount) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction({ type: 'unstake', amount: BigInt(amount) }, createWasmContext()); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for withdrawUnbonded', async function () { + const slashingSpans = 0; + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getWithdrawUnstakedBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .slashingSpans(slashingSpans) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction({ type: 'withdrawUnbonded', slashingSpans }, createWasmContext()); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for chill (unnominate)', async function () { + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getUnnominateBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction({ type: 'chill' }, createWasmContext()); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + }); + + // =========================================================================== + // Proxy Transaction Tests + // =========================================================================== + describe('Proxy Transactions', function () { + it('should produce identical signing payload for addProxy', async function () { + const delegate = accounts.account2.address; + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getAddressInitializationBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .owner({ address: delegate }) + .type(ProxyType.ANY) + .delay('0') + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction({ type: 'addProxy', delegate, proxyType: 'Any', delay: 0 }, createWasmContext()); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for addProxy with Staking type', async function () { + const delegate = accounts.account2.address; + + const factory = new TransactionBuilderFactory(coin); + const legacyBuilder = factory.getAddressInitializationBuilder(); + + legacyBuilder + .sender({ address: accounts.account1.address }) + .owner({ address: delegate }) + .type(ProxyType.STAKING) + .delay('100') + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await legacyBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { type: 'addProxy', delegate, proxyType: 'Staking', delay: 100 }, + createWasmContext() + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + }); + + // =========================================================================== + // Batch Transaction Tests + // =========================================================================== + describe('Batch Transactions', function () { + it('should produce identical signing payload for batch of transfers', async function () { + const to1 = accounts.account2.address; + const to2 = accounts.account3.address; + const amount1 = '1000000000000'; + const amount2 = '2000000000000'; + + // Legacy batch requires raw call hex for each sub-call + const factory = new TransactionBuilderFactory(coin); + + // Build individual transfers to get their call data for the legacy batch + const t1 = factory.getTransferBuilder() as TransferBuilder; + t1.sender({ address: accounts.account1.address }) + .to({ address: to1 }) + .amount(amount1) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + const tx1 = await t1.build(); + + const t2 = factory.getTransferBuilder() as TransferBuilder; + t2.sender({ address: accounts.account1.address }) + .to({ address: to2 }) + .amount(amount2) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + const tx2 = await t2.build(); + + // Extract call data from legacy signing payload for batch construction. + // Legacy toBroadcastFormat() for unsigned txs returns the signing payload + // which starts with a compact-length-prefixed call. + function extractCallFromSigningPayload(signingPayload: string): string { + const hexData = signingPayload.startsWith('0x') ? signingPayload.slice(2) : signingPayload; + const bytes = Buffer.from(hexData, 'hex'); + // Decode compact length prefix to find where call data ends + const mode = bytes[0] & 0b11; + let callLength: number; + let offset: number; + if (mode === 0b00) { + callLength = bytes[0] >> 2; + offset = 1; + } else if (mode === 0b01) { + callLength = (bytes[0] | (bytes[1] << 8)) >> 2; + offset = 2; + } else if (mode === 0b10) { + callLength = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)) >> 2; + offset = 4; + } else { + throw new Error('Unsupported compact length mode'); + } + return '0x' + bytes.slice(offset, offset + callLength).toString('hex'); + } + + const call1Hex = extractCallFromSigningPayload(tx1.toBroadcastFormat()); + const call2Hex = extractCallFromSigningPayload(tx2.toBroadcastFormat()); + + const batchBuilder = factory.getBatchTransactionBuilder(); + batchBuilder + .sender({ address: accounts.account1.address }) + .calls([call1Hex, call2Hex]) + .atomic(true) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await batchBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'transfer', to: to1, amount: BigInt(amount1), keepAlive: true }, + { type: 'transfer', to: to2, amount: BigInt(amount2), keepAlive: true }, + ], + atomic: true, + }, + createWasmContext() + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + + it('should produce identical signing payload for non-atomic batch', async function () { + const to = accounts.account2.address; + const amount = '1000000000000'; + + const factory = new TransactionBuilderFactory(coin); + + const transferBuilder = factory.getTransferBuilder() as TransferBuilder; + transferBuilder + .sender({ address: accounts.account1.address }) + .to({ address: to }) + .amount(amount) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + const transferTx = await transferBuilder.build(); + + function extractCallFromSigningPayload(signingPayload: string): string { + const hexData = signingPayload.startsWith('0x') ? signingPayload.slice(2) : signingPayload; + const bytes = Buffer.from(hexData, 'hex'); + const mode = bytes[0] & 0b11; + let callLength: number; + let offset: number; + if (mode === 0b00) { + callLength = bytes[0] >> 2; + offset = 1; + } else if (mode === 0b01) { + callLength = (bytes[0] | (bytes[1] << 8)) >> 2; + offset = 2; + } else if (mode === 0b10) { + callLength = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)) >> 2; + offset = 4; + } else { + throw new Error('Unsupported compact length mode'); + } + return '0x' + bytes.slice(offset, offset + callLength).toString('hex'); + } + + const callHex = extractCallFromSigningPayload(transferTx.toBroadcastFormat()); + + const batchBuilder = factory.getBatchTransactionBuilder(); + batchBuilder + .sender({ address: accounts.account1.address }) + .calls([callHex]) + .atomic(false) + .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) + .referenceBlock(westendBlock.hash) + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); + + const legacyTx = await batchBuilder.build(); + const legacyHex = legacyTx.toBroadcastFormat(); + + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [{ type: 'transfer', to, amount: BigInt(amount), keepAlive: true }], + atomic: false, + }, + createWasmContext() + ); + const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); + + assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); + }); + }); + + // =========================================================================== + // Intent-based Transaction Building (sanity checks) + // =========================================================================== + describe('Intent-based Transaction Building', function () { + it('should build transfer from intent', async function () { + const wasmTx = buildTransaction( + { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, + createWasmContext() + ); + const serialized = wasmTx.toBroadcastFormat(); + assert(serialized.startsWith('0x'), 'Should be hex encoded'); + assert(serialized.length > 10, 'Should have content'); + }); + + it('should build stake from intent', async function () { + const wasmTx = buildTransaction( + { type: 'stake', amount: 5000000000000n, payee: { type: 'staked' } }, + createWasmContext() + ); + assert(wasmTx.toBroadcastFormat().startsWith('0x')); + }); + + it('should build withdrawUnbonded from intent', async function () { + const wasmTx = buildTransaction({ type: 'withdrawUnbonded', slashingSpans: 0 }, createWasmContext()); + assert(wasmTx.toBroadcastFormat().startsWith('0x')); + }); + + it('should build chill from intent', async function () { + const wasmTx = buildTransaction({ type: 'chill' }, createWasmContext()); + assert(wasmTx.toBroadcastFormat().startsWith('0x')); + }); + + it('should build addProxy from intent', async function () { + const wasmTx = buildTransaction( + { type: 'addProxy', delegate: accounts.account2.address, proxyType: 'Any', delay: 0 }, + createWasmContext() + ); + assert(wasmTx.toBroadcastFormat().startsWith('0x')); + }); + + it('should build batch from intent', async function () { + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, + { type: 'chill' }, + ], + atomic: true, + }, + createWasmContext() + ); + assert(wasmTx.toBroadcastFormat().startsWith('0x')); + }); + }); +}); diff --git a/webpack/bitgojs.config.js b/webpack/bitgojs.config.js index 04c9ce5fa5..1839c798b5 100644 --- a/webpack/bitgojs.config.js +++ b/webpack/bitgojs.config.js @@ -18,6 +18,7 @@ module.exports = { // Force ESM versions for browser bundles - required for proper WASM initialization. // Note: We can't use global `conditionNames: ['browser', 'import', ...]` because // third-party packages like @solana/spl-token and @bufbuild/protobuf have broken ESM builds. + '@bitgo/wasm-dot': path.resolve('../../node_modules/@bitgo/wasm-dot/dist/esm/js/index.js'), '@bitgo/wasm-utxo': path.resolve('../../node_modules/@bitgo/wasm-utxo/dist/esm/js/index.js'), '@bitgo/wasm-solana': path.resolve('../../node_modules/@bitgo/wasm-solana/dist/esm/js/index.js'), '@bitgo/utxo-ord': path.resolve('../utxo-ord/dist/esm/index.js'), diff --git a/yarn.lock b/yarn.lock index 5ccd99cb96..330fc26f77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -985,6 +985,11 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" +"@bitgo/wasm-dot@^1.1.1", "@bitgo/wasm-dot@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.1.2.tgz#af1b190ea684838a512e47f4036c453285229bb2" + integrity sha512-zVGsDG+eFpaEnlVbpniSB8ANms/BQgl54pMcsaPJmzrCwpN56CxEWdYyvsvfdNuOC6Le3zvx/6KF75RdmFxnCA== + "@bitgo/wasm-solana@^2.6.0": version "2.6.0" resolved "https://registry.npmjs.org/@bitgo/wasm-solana/-/wasm-solana-2.6.0.tgz#c8b57ab010f22f1a1c90681cd180814c4ec2867b" From 309fa9569126c78eacd7b7dae4d2512c106531f7 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Mon, 9 Mar 2026 18:12:17 -0700 Subject: [PATCH 2/3] feat: use proxy deposit cost for batch stake/unstake explanations When explaining batch transactions containing proxy operations (removeProxy+chill+unbond for unstake, bond+addProxy for stake), use getProxyDepositCost() from @bitgo/wasm-dot to match legacy account-lib behavior. Bumps @bitgo/wasm-dot dep to ^1.2.0. Ticket: BTC-3062 --- modules/sdk-coin-dot/package.json | 2 +- modules/sdk-coin-dot/src/lib/wasmParser.ts | 78 +++++- .../test/unit/wasmParserExplanation.ts | 237 ++++++++++++++++++ 3 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts diff --git a/modules/sdk-coin-dot/package.json b/modules/sdk-coin-dot/package.json index 854e67fe7b..8587516d76 100644 --- a/modules/sdk-coin-dot/package.json +++ b/modules/sdk-coin-dot/package.json @@ -43,7 +43,7 @@ "@bitgo/sdk-core": "^36.33.2", "@bitgo/sdk-lib-mpc": "^10.9.0", "@bitgo/statics": "^58.29.0", - "@bitgo/wasm-dot": "^1.1.2", + "@bitgo/wasm-dot": "^1.2.0", "@polkadot/api": "14.1.1", "@polkadot/api-augment": "14.1.1", "@polkadot/keyring": "13.5.6", diff --git a/modules/sdk-coin-dot/src/lib/wasmParser.ts b/modules/sdk-coin-dot/src/lib/wasmParser.ts index 3cc289f5b7..a2f4d3bfd6 100644 --- a/modules/sdk-coin-dot/src/lib/wasmParser.ts +++ b/modules/sdk-coin-dot/src/lib/wasmParser.ts @@ -7,7 +7,7 @@ */ import { TransactionType } from '@bitgo/sdk-core'; -import { DotTransaction, parseTransaction, type ParsedMethod, type Era } from '@bitgo/wasm-dot'; +import { DotTransaction, parseTransaction, getProxyDepositCost, type ParsedMethod, type Era } from '@bitgo/wasm-dot'; import type { BatchCallObject, ProxyType, TransactionExplanation, Material, TxData } from './iface'; const MAX_NESTING_DEPTH = 10; @@ -180,11 +180,30 @@ function buildExplanation(params: { const parsed = parseTransaction(tx, context); const typeName = deriveTransactionType(parsed.method, 0); - const outputs = extractOutputs(parsed.method, 0); const sender = parsed.sender ?? params.senderAddress; - const inputs: { address: string; value: string }[] = sender - ? outputs.map((o) => ({ address: sender, value: o.amount })) - : []; + + // Check for batch patterns that need proxy deposit cost handling + const batchInfo = detectProxyBatch(parsed.method); + let outputs: { address: string; amount: string }[]; + let inputs: { address: string; value: string }[]; + + if (batchInfo && sender) { + const proxyDepositCost = getProxyDepositCost(params.material.metadata).toString(); + if (batchInfo.type === 'unstake') { + // Unstaking batch (removeProxy + chill + unbond): proxy deposit refund flows + // from proxy address back to sender. The unbond amount is NOT in inputs/outputs. + outputs = [{ address: sender, amount: proxyDepositCost }]; + inputs = [{ address: batchInfo.proxyAddress, value: proxyDepositCost }]; + } else { + // Staking batch (bond + addProxy): bond amount + proxy deposit cost + const bondOutputs = extractOutputs(parsed.method, 0).filter((o) => o.address === 'STAKING'); + outputs = [...bondOutputs, { address: batchInfo.proxyAddress, amount: proxyDepositCost }]; + inputs = outputs.map((o) => ({ address: sender, value: o.amount })); + } + } else { + outputs = extractOutputs(parsed.method, 0); + inputs = sender ? outputs.map((o) => ({ address: sender, value: o.amount })) : []; + } const outputAmount = outputs.reduce((sum, o) => { if (o.amount === 'ALL') return sum; @@ -297,6 +316,55 @@ function extractOutputs(method: ParsedMethod, depth: number): { address: string; } } +// ============================================================================= +// Batch proxy detection +// ============================================================================= + +interface ProxyBatchInfo { + type: 'stake' | 'unstake'; + proxyAddress: string; +} + +/** + * Detect batch transactions involving proxy operations (stake/unstake batches). + * + * Legacy account-lib reports proxy deposit cost (ProxyDepositBase + ProxyDepositFactor) + * as the value for these batches instead of the bond/unbond amount. + * + * Unstaking batch: removeProxy + chill + unbond + * Staking batch: bond + addProxy + */ +function detectProxyBatch(method: ParsedMethod): ProxyBatchInfo | undefined { + const key = `${method.pallet}.${method.name}`; + if (key !== 'utility.batch' && key !== 'utility.batchAll') return undefined; + + const calls = ((method.args ?? {}) as Record).calls as ParsedMethod[] | undefined; + if (!calls || calls.length === 0) return undefined; + + const callKeys = calls.map((c) => `${c.pallet}.${c.name}`); + + // Unstaking batch: removeProxy + chill + unbond (3 calls) + if ( + calls.length === 3 && + callKeys[0] === 'proxy.removeProxy' && + callKeys[1] === 'staking.chill' && + callKeys[2] === 'staking.unbond' + ) { + const removeProxyArgs = (calls[0].args ?? {}) as Record; + const proxyAddress = String(removeProxyArgs.delegate ?? ''); + return { type: 'unstake', proxyAddress }; + } + + // Staking batch: bond + addProxy (2 calls) + if (calls.length === 2 && callKeys[0] === 'staking.bond' && callKeys[1] === 'proxy.addProxy') { + const addProxyArgs = (calls[1].args ?? {}) as Record; + const proxyAddress = String(addProxyArgs.delegate ?? ''); + return { type: 'stake', proxyAddress }; + } + + return undefined; +} + // ============================================================================= // Helpers // ============================================================================= diff --git a/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts b/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts new file mode 100644 index 0000000000..ab0da5adc6 --- /dev/null +++ b/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts @@ -0,0 +1,237 @@ +/** + * WASM Parser Explanation Tests + * + * Tests for explainDotTransaction, specifically verifying batch transaction + * handling with proxy deposit costs matches legacy account-lib behavior. + * + * Uses WASM-built transactions (not legacy rawTx fixtures) since the WASM + * parser requires metadata-compatible signed extension encoding. + */ + +import assert from 'assert'; +import { coins } from '@bitgo/statics'; +import { TransactionType } from '@bitgo/sdk-core'; +import { explainDotTransaction } from '../../src/lib/wasmParser'; +import { buildTransaction, type BuildContext, type Material as WasmMaterial } from '@bitgo/wasm-dot'; +import type { Material } from '../../src/lib/iface'; +import { accounts, westendBlock } from '../fixtures'; +import utils from '../../src/lib/utils'; + +describe('WASM Parser Explanation', function () { + const coin = coins.get('tdot'); + // utils.getMaterial returns the iface Material shape; cast to WasmMaterial for buildTransaction + const material = utils.getMaterial(coin) as Material & WasmMaterial; + + function createWasmContext(overrides: Partial = {}): BuildContext { + return { + sender: accounts.account1.address, + nonce: 0, + tip: 0n, + material, + validity: { + firstValid: westendBlock.blockNumber, + maxDuration: 2400, + }, + referenceBlock: westendBlock.hash, + ...overrides, + }; + } + + describe('Batch unstake (removeProxy + chill + unbond)', function () { + it('should explain batch unstake with proxy deposit cost', function () { + const unbondAmount = 5000000000000n; // 5 DOT + const proxyDelegate = accounts.account2.address; + + // Build a batch unstake tx: removeProxy + chill + unbond + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'removeProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, + { type: 'chill' }, + { type: 'unstake', amount: unbondAmount }, + ], + atomic: true, + }, + createWasmContext() + ); + + const txHex = wasmTx.toBroadcastFormat(); + const explanation = explainDotTransaction({ + txHex, + material, + senderAddress: accounts.account1.address, + }); + + // Should be Batch type + assert.strictEqual(explanation.type, TransactionType.Batch); + assert.ok(explanation.methodName.includes('batchAll'), `Expected batchAll, got ${explanation.methodName}`); + + // Outputs should contain proxy deposit cost, NOT the unbond amount + assert.strictEqual(explanation.outputs.length, 1, 'Should have exactly one output (proxy deposit cost)'); + const output = explanation.outputs[0]; + assert.strictEqual(output.address, accounts.account1.address, 'Output should go to sender (deposit refund)'); + const proxyDepositCost = BigInt(output.amount); + assert.ok(proxyDepositCost > 0n, 'Proxy deposit cost should be positive'); + // The proxy deposit cost should NOT equal the unbond amount + assert.notStrictEqual(proxyDepositCost, unbondAmount, 'Should use proxy deposit cost, not unbond amount'); + + // Input should come from the proxy delegate address + assert.strictEqual(explanation.inputs.length, 1, 'Should have exactly one input'); + assert.strictEqual(explanation.inputs[0].address, proxyDelegate, 'Input should come from proxy delegate'); + assert.strictEqual( + explanation.inputs[0].valueString, + output.amount, + 'Input value should equal proxy deposit cost' + ); + }); + + it('proxy deposit cost should be consistent across calls', function () { + const proxyDelegate = accounts.account2.address; + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'removeProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, + { type: 'chill' }, + { type: 'unstake', amount: 1000000000000n }, + ], + atomic: true, + }, + createWasmContext() + ); + + const txHex = wasmTx.toBroadcastFormat(); + const explanation1 = explainDotTransaction({ txHex, material, senderAddress: accounts.account1.address }); + const explanation2 = explainDotTransaction({ txHex, material, senderAddress: accounts.account1.address }); + + assert.strictEqual(explanation1.outputs[0].amount, explanation2.outputs[0].amount); + }); + + it('should work with non-atomic batch (utility.batch)', function () { + const proxyDelegate = accounts.account2.address; + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'removeProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, + { type: 'chill' }, + { type: 'unstake', amount: 3000000000000n }, + ], + atomic: false, + }, + createWasmContext() + ); + + const txHex = wasmTx.toBroadcastFormat(); + const explanation = explainDotTransaction({ txHex, material, senderAddress: accounts.account1.address }); + + assert.strictEqual(explanation.type, TransactionType.Batch); + assert.strictEqual(explanation.outputs.length, 1); + assert.ok(BigInt(explanation.outputs[0].amount) > 0n); + assert.strictEqual(explanation.inputs[0].address, proxyDelegate); + }); + }); + + describe('Batch stake (bond + addProxy)', function () { + it('should explain batch stake with bond amount and proxy deposit cost', function () { + const bondAmount = 10000000000000n; // 10 DOT + const proxyDelegate = accounts.account2.address; + + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'stake', amount: bondAmount, payee: { type: 'staked' } }, + { type: 'addProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, + ], + atomic: true, + }, + createWasmContext() + ); + + const txHex = wasmTx.toBroadcastFormat(); + const explanation = explainDotTransaction({ txHex, material, senderAddress: accounts.account1.address }); + + assert.strictEqual(explanation.type, TransactionType.Batch); + + // Should have two outputs: bond amount (STAKING) + proxy deposit cost (to proxy delegate) + assert.strictEqual(explanation.outputs.length, 2, 'Should have bond + proxy deposit outputs'); + + const stakingOutput = explanation.outputs.find((o) => o.address === 'STAKING'); + assert.ok(stakingOutput, 'Should have STAKING output for bond amount'); + assert.strictEqual(BigInt(stakingOutput!.amount), bondAmount, 'Bond amount should match'); + + const proxyOutput = explanation.outputs.find((o) => o.address !== 'STAKING'); + assert.ok(proxyOutput, 'Should have proxy deposit output'); + assert.strictEqual(proxyOutput!.address, proxyDelegate); + assert.ok(BigInt(proxyOutput!.amount) > 0n, 'Proxy deposit cost should be positive'); + + // All inputs should come from sender + assert.strictEqual(explanation.inputs.length, 2); + for (const input of explanation.inputs) { + assert.strictEqual(input.address, accounts.account1.address); + } + }); + }); + + describe('Non-batch transactions (should not be affected)', function () { + it('should explain transfer normally', function () { + const wasmTx = buildTransaction( + { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, + createWasmContext() + ); + + const explanation = explainDotTransaction({ + txHex: wasmTx.toBroadcastFormat(), + material, + senderAddress: accounts.account1.address, + }); + + assert.strictEqual(explanation.type, TransactionType.Send); + assert.strictEqual(explanation.outputs.length, 1); + assert.strictEqual(explanation.outputs[0].address, accounts.account2.address); + assert.strictEqual(explanation.outputs[0].amount, '1000000000000'); + }); + + it('should explain single unstake (unbond) normally', function () { + const wasmTx = buildTransaction({ type: 'unstake', amount: 5000000000000n }, createWasmContext()); + + const explanation = explainDotTransaction({ + txHex: wasmTx.toBroadcastFormat(), + material, + senderAddress: accounts.account1.address, + }); + + assert.strictEqual(explanation.type, TransactionType.StakingUnlock); + assert.strictEqual(explanation.outputs.length, 1); + assert.strictEqual(explanation.outputs[0].address, 'STAKING'); + assert.strictEqual(explanation.outputs[0].amount, '5000000000000'); + }); + + it('should explain batch of transfers normally (no proxy involved)', function () { + const wasmTx = buildTransaction( + { + type: 'batch', + calls: [ + { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, + { type: 'transfer', to: accounts.account3.address, amount: 2000000000000n, keepAlive: true }, + ], + atomic: true, + }, + createWasmContext() + ); + + const explanation = explainDotTransaction({ + txHex: wasmTx.toBroadcastFormat(), + material, + senderAddress: accounts.account1.address, + }); + + assert.strictEqual(explanation.type, TransactionType.Batch); + assert.strictEqual(explanation.outputs.length, 2); + assert.strictEqual(explanation.outputs[0].amount, '1000000000000'); + assert.strictEqual(explanation.outputs[1].amount, '2000000000000'); + }); + }); +}); From eb8248948adc4be7b60c881a3d863fc54b979db8 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Tue, 10 Mar 2026 10:41:35 -0700 Subject: [PATCH 3/3] feat: update wasm-dot to ^1.3.0 and migrate tests to high-level intents wasm-dot@1.3.0 replaces low-level call types (transfer, batch, addProxy, removeProxy, chill, withdrawUnbonded) with high-level business intents (payment, stake, unstake, claim, consolidate). Batching and proxy management are now handled automatically by the crate based on intent composition. Changes: - Update @bitgo/wasm-dot dependency from ^1.2.0 to ^1.3.0 - Migrate dot.ts tests: transfer -> payment - Migrate wasmBuilderByteComparison.ts: remove byte comparisons for bond (now produced only via stake+proxyAddress batch), replace low-level batch/proxy/chill tests with high-level intent sanity checks - Migrate wasmParserExplanation.ts: use unstake+stopStaking for full unstake batches, stake+proxyAddress for new stake batches Ticket: BTC-3062 --- modules/sdk-coin-dot/package.json | 2 +- modules/sdk-coin-dot/src/lib/wasmParser.ts | 35 +- modules/sdk-coin-dot/test/unit/dot.ts | 7 +- .../test/unit/wasmBuilderByteComparison.ts | 341 ++---------------- .../test/unit/wasmParserExplanation.ts | 105 ++---- yarn.lock | 291 ++++++++------- 6 files changed, 246 insertions(+), 535 deletions(-) diff --git a/modules/sdk-coin-dot/package.json b/modules/sdk-coin-dot/package.json index 8587516d76..2b44a493b3 100644 --- a/modules/sdk-coin-dot/package.json +++ b/modules/sdk-coin-dot/package.json @@ -43,7 +43,7 @@ "@bitgo/sdk-core": "^36.33.2", "@bitgo/sdk-lib-mpc": "^10.9.0", "@bitgo/statics": "^58.29.0", - "@bitgo/wasm-dot": "^1.2.0", + "@bitgo/wasm-dot": "^1.3.0", "@polkadot/api": "14.1.1", "@polkadot/api-augment": "14.1.1", "@polkadot/keyring": "13.5.6", diff --git a/modules/sdk-coin-dot/src/lib/wasmParser.ts b/modules/sdk-coin-dot/src/lib/wasmParser.ts index a2f4d3bfd6..9cd9b53330 100644 --- a/modules/sdk-coin-dot/src/lib/wasmParser.ts +++ b/modules/sdk-coin-dot/src/lib/wasmParser.ts @@ -12,6 +12,17 @@ import type { BatchCallObject, ProxyType, TransactionExplanation, Material, TxDa const MAX_NESTING_DEPTH = 10; +/** + * Display-only sentinel address for staking outputs (bond, bondExtra, unbond). + * + * Staking extrinsics don't transfer funds to an external address. The bonded DOT + * stays in the sender's account as "bonded balance". But the explanation format + * requires an output address, so we use this null address (SS58 encoding of 32 + * zero bytes, substrate generic prefix 42) as a placeholder meaning "this amount + * went to staking". Same constant used by the legacy account-lib Transaction class. + */ +const STAKING_DESTINATION = '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'; + // ============================================================================= // Public types // ============================================================================= @@ -195,9 +206,12 @@ function buildExplanation(params: { outputs = [{ address: sender, amount: proxyDepositCost }]; inputs = [{ address: batchInfo.proxyAddress, value: proxyDepositCost }]; } else { - // Staking batch (bond + addProxy): bond amount + proxy deposit cost - const bondOutputs = extractOutputs(parsed.method, 0).filter((o) => o.address === 'STAKING'); - outputs = [...bondOutputs, { address: batchInfo.proxyAddress, amount: proxyDepositCost }]; + // Staking batch (bond + addProxy): bond amount to staking destination + proxy deposit cost + const bondAmount = extractBondAmount(parsed.method); + outputs = [ + { address: STAKING_DESTINATION, amount: bondAmount }, + { address: batchInfo.proxyAddress, amount: proxyDepositCost }, + ]; inputs = outputs.map((o) => ({ address: sender, value: o.amount })); } } else { @@ -296,7 +310,7 @@ function extractOutputs(method: ParsedMethod, depth: number): { address: string; case 'staking.bond': case 'staking.bondExtra': case 'staking.unbond': - return [{ address: 'STAKING', amount: String(args.value ?? '0') }]; + return [{ address: STAKING_DESTINATION, amount: String(args.value ?? '0') }]; case 'utility.batch': case 'utility.batchAll': { @@ -316,6 +330,19 @@ function extractOutputs(method: ParsedMethod, depth: number): { address: string; } } +// ============================================================================= +// Batch helpers +// ============================================================================= + +/** Extract bond amount from a batch method containing a staking.bond call */ +function extractBondAmount(method: ParsedMethod): string { + const calls = ((method.args ?? {}) as Record).calls as ParsedMethod[] | undefined; + const bondCall = calls?.find((c) => `${c.pallet}.${c.name}` === 'staking.bond'); + if (!bondCall) return '0'; + const args = (bondCall.args ?? {}) as Record; + return String(args.value ?? '0'); +} + // ============================================================================= // Batch proxy detection // ============================================================================= diff --git a/modules/sdk-coin-dot/test/unit/dot.ts b/modules/sdk-coin-dot/test/unit/dot.ts index 004f2807d4..1581c5bf40 100644 --- a/modules/sdk-coin-dot/test/unit/dot.ts +++ b/modules/sdk-coin-dot/test/unit/dot.ts @@ -206,7 +206,7 @@ describe('DOT:', function () { } it('should explain a transfer via explainDotTransaction', function () { - const tx = buildTransaction({ type: 'transfer', to: RECIPIENT, amount: 1000000000000n }, wasmContext()); + const tx = buildTransaction({ type: 'payment', to: RECIPIENT, amount: 1000000000000n }, wasmContext()); const explained = explainDotTransaction({ txHex: tx.toBroadcastFormat(), material, @@ -232,12 +232,13 @@ describe('DOT:', function () { assert.strictEqual(explained.type, TransactionType.StakingActivate); assert.strictEqual(explained.outputs.length, 1); - assert.strictEqual(explained.outputs[0].address, 'STAKING'); + // STAKING_DESTINATION: SS58(0x00..00) sentinel, bond doesn't transfer to an external address + assert.strictEqual(explained.outputs[0].address, '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'); assert.strictEqual(explained.outputs[0].amount, '5000000000000'); }); it('should explain a transfer via explainDotTransaction with sender', function () { - const tx = buildTransaction({ type: 'transfer', to: RECIPIENT, amount: 1000000000000n }, wasmContext()); + const tx = buildTransaction({ type: 'payment', to: RECIPIENT, amount: 1000000000000n }, wasmContext()); const explained = explainDotTransaction({ txHex: tx.toBroadcastFormat(), material, diff --git a/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts b/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts index c710a6959f..bd4beeb7fa 100644 --- a/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts +++ b/modules/sdk-coin-dot/test/unit/wasmBuilderByteComparison.ts @@ -3,7 +3,7 @@ * * Compare serialized output between: * 1. Legacy approach (using @substrate/txwrapper-polkadot) - * 2. WASM approach (using @bitgo/wasm-dot DotBuilder) + * 2. WASM approach (using @bitgo/wasm-dot buildTransaction) * * For unsigned transactions, legacy toBroadcastFormat() returns the signing payload * (via construct.signingPayload). We compare WASM signablePayload() against it. @@ -12,13 +12,17 @@ * a SCALE compact-length prefix. subxt encodes it as raw `Call` (no prefix). * We strip this prefix from the legacy side before comparing, since the actual * call data, era, nonce, tip, and chain context are identical. + * + * Note: wasm-dot@1.3.0 uses high-level business intents (payment, stake, unstake, + * claim, consolidate) instead of low-level call types. Batch and proxy operations + * are composed automatically from the intent (e.g., stake with proxyAddress + * produces batchAll(bond, addProxy)). */ import assert from 'assert'; import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; import { TransferBuilder } from '../../src/lib/transferBuilder'; -import { ProxyType } from '../../src/lib/iface'; import { accounts, westendBlock } from '../fixtures'; import utils from '../../src/lib/utils'; @@ -88,7 +92,7 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { const legacyHex = legacyTx.toBroadcastFormat(); const wasmTx = buildTransaction( - { type: 'transfer', to, amount: BigInt(amount), keepAlive: true }, + { type: 'payment', to, amount: BigInt(amount), keepAlive: true }, createWasmContext() ); const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); @@ -115,7 +119,7 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { const legacyHex = legacyTx.toBroadcastFormat(); const wasmTx = buildTransaction( - { type: 'transfer', to, amount: BigInt(amount), keepAlive: true }, + { type: 'payment', to, amount: BigInt(amount), keepAlive: true }, createWasmContext({ nonce: 5 }) ); const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); @@ -128,57 +132,11 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { // Staking Transaction Tests // =========================================================================== describe('Staking Transactions', function () { - it('should produce identical signing payload for staking bond', async function () { - const amount = '10000000000000'; // 10 DOT - - const factory = new TransactionBuilderFactory(coin); - const legacyBuilder = factory.getStakingBuilder(); - - legacyBuilder - .sender({ address: accounts.account1.address }) - .amount(amount) - .payee('Staked') - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await legacyBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction( - { type: 'stake', amount: BigInt(amount), payee: { type: 'staked' } }, - createWasmContext() - ); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); - - it('should produce identical signing payload for staking bond with Stash payee', async function () { - const amount = '20000000000000'; // 20 DOT - - const factory = new TransactionBuilderFactory(coin); - const legacyBuilder = factory.getStakingBuilder(); - - legacyBuilder - .sender({ address: accounts.account1.address }) - .amount(amount) - .payee('Stash') - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await legacyBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction( - { type: 'stake', amount: BigInt(amount), payee: { type: 'stash' } }, - createWasmContext() - ); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); + // Note: wasm-dot@1.3.0 `stake` without proxyAddress produces bondExtra (top-up), + // while the legacy getStakingBuilder() produces bond (initial stake). These are + // different extrinsic calls so byte comparison is not applicable. The bond call + // is now only produced via `stake` with proxyAddress (which creates a batch). + // See Intent-based Transaction Building tests below for stake sanity checks. it('should produce identical signing payload for unstake (unbond)', async function () { const amount = '5000000000000'; // 5 DOT @@ -202,7 +160,7 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); }); - it('should produce identical signing payload for withdrawUnbonded', async function () { + it('should produce identical signing payload for withdrawUnbonded (claim)', async function () { const slashingSpans = 0; const factory = new TransactionBuilderFactory(coin); @@ -218,235 +176,7 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { const legacyTx = await legacyBuilder.build(); const legacyHex = legacyTx.toBroadcastFormat(); - const wasmTx = buildTransaction({ type: 'withdrawUnbonded', slashingSpans }, createWasmContext()); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); - - it('should produce identical signing payload for chill (unnominate)', async function () { - const factory = new TransactionBuilderFactory(coin); - const legacyBuilder = factory.getUnnominateBuilder(); - - legacyBuilder - .sender({ address: accounts.account1.address }) - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await legacyBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction({ type: 'chill' }, createWasmContext()); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); - }); - - // =========================================================================== - // Proxy Transaction Tests - // =========================================================================== - describe('Proxy Transactions', function () { - it('should produce identical signing payload for addProxy', async function () { - const delegate = accounts.account2.address; - - const factory = new TransactionBuilderFactory(coin); - const legacyBuilder = factory.getAddressInitializationBuilder(); - - legacyBuilder - .sender({ address: accounts.account1.address }) - .owner({ address: delegate }) - .type(ProxyType.ANY) - .delay('0') - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await legacyBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction({ type: 'addProxy', delegate, proxyType: 'Any', delay: 0 }, createWasmContext()); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); - - it('should produce identical signing payload for addProxy with Staking type', async function () { - const delegate = accounts.account2.address; - - const factory = new TransactionBuilderFactory(coin); - const legacyBuilder = factory.getAddressInitializationBuilder(); - - legacyBuilder - .sender({ address: accounts.account1.address }) - .owner({ address: delegate }) - .type(ProxyType.STAKING) - .delay('100') - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await legacyBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction( - { type: 'addProxy', delegate, proxyType: 'Staking', delay: 100 }, - createWasmContext() - ); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); - }); - - // =========================================================================== - // Batch Transaction Tests - // =========================================================================== - describe('Batch Transactions', function () { - it('should produce identical signing payload for batch of transfers', async function () { - const to1 = accounts.account2.address; - const to2 = accounts.account3.address; - const amount1 = '1000000000000'; - const amount2 = '2000000000000'; - - // Legacy batch requires raw call hex for each sub-call - const factory = new TransactionBuilderFactory(coin); - - // Build individual transfers to get their call data for the legacy batch - const t1 = factory.getTransferBuilder() as TransferBuilder; - t1.sender({ address: accounts.account1.address }) - .to({ address: to1 }) - .amount(amount1) - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - const tx1 = await t1.build(); - - const t2 = factory.getTransferBuilder() as TransferBuilder; - t2.sender({ address: accounts.account1.address }) - .to({ address: to2 }) - .amount(amount2) - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - const tx2 = await t2.build(); - - // Extract call data from legacy signing payload for batch construction. - // Legacy toBroadcastFormat() for unsigned txs returns the signing payload - // which starts with a compact-length-prefixed call. - function extractCallFromSigningPayload(signingPayload: string): string { - const hexData = signingPayload.startsWith('0x') ? signingPayload.slice(2) : signingPayload; - const bytes = Buffer.from(hexData, 'hex'); - // Decode compact length prefix to find where call data ends - const mode = bytes[0] & 0b11; - let callLength: number; - let offset: number; - if (mode === 0b00) { - callLength = bytes[0] >> 2; - offset = 1; - } else if (mode === 0b01) { - callLength = (bytes[0] | (bytes[1] << 8)) >> 2; - offset = 2; - } else if (mode === 0b10) { - callLength = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)) >> 2; - offset = 4; - } else { - throw new Error('Unsupported compact length mode'); - } - return '0x' + bytes.slice(offset, offset + callLength).toString('hex'); - } - - const call1Hex = extractCallFromSigningPayload(tx1.toBroadcastFormat()); - const call2Hex = extractCallFromSigningPayload(tx2.toBroadcastFormat()); - - const batchBuilder = factory.getBatchTransactionBuilder(); - batchBuilder - .sender({ address: accounts.account1.address }) - .calls([call1Hex, call2Hex]) - .atomic(true) - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await batchBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction( - { - type: 'batch', - calls: [ - { type: 'transfer', to: to1, amount: BigInt(amount1), keepAlive: true }, - { type: 'transfer', to: to2, amount: BigInt(amount2), keepAlive: true }, - ], - atomic: true, - }, - createWasmContext() - ); - const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); - - assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); - }); - - it('should produce identical signing payload for non-atomic batch', async function () { - const to = accounts.account2.address; - const amount = '1000000000000'; - - const factory = new TransactionBuilderFactory(coin); - - const transferBuilder = factory.getTransferBuilder() as TransferBuilder; - transferBuilder - .sender({ address: accounts.account1.address }) - .to({ address: to }) - .amount(amount) - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - const transferTx = await transferBuilder.build(); - - function extractCallFromSigningPayload(signingPayload: string): string { - const hexData = signingPayload.startsWith('0x') ? signingPayload.slice(2) : signingPayload; - const bytes = Buffer.from(hexData, 'hex'); - const mode = bytes[0] & 0b11; - let callLength: number; - let offset: number; - if (mode === 0b00) { - callLength = bytes[0] >> 2; - offset = 1; - } else if (mode === 0b01) { - callLength = (bytes[0] | (bytes[1] << 8)) >> 2; - offset = 2; - } else if (mode === 0b10) { - callLength = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)) >> 2; - offset = 4; - } else { - throw new Error('Unsupported compact length mode'); - } - return '0x' + bytes.slice(offset, offset + callLength).toString('hex'); - } - - const callHex = extractCallFromSigningPayload(transferTx.toBroadcastFormat()); - - const batchBuilder = factory.getBatchTransactionBuilder(); - batchBuilder - .sender({ address: accounts.account1.address }) - .calls([callHex]) - .atomic(false) - .validity({ firstValid: westendBlock.blockNumber, maxDuration: 2400 }) - .referenceBlock(westendBlock.hash) - .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 0 }); - - const legacyTx = await batchBuilder.build(); - const legacyHex = legacyTx.toBroadcastFormat(); - - const wasmTx = buildTransaction( - { - type: 'batch', - calls: [{ type: 'transfer', to, amount: BigInt(amount), keepAlive: true }], - atomic: false, - }, - createWasmContext() - ); + const wasmTx = buildTransaction({ type: 'claim', slashingSpans }, createWasmContext()); const wasmHex = '0x' + Buffer.from(wasmTx.signablePayload()).toString('hex'); assert.strictEqual(wasmHex, stripCompactPrefix(legacyHex), 'Signing payload should match'); @@ -457,9 +187,9 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { // Intent-based Transaction Building (sanity checks) // =========================================================================== describe('Intent-based Transaction Building', function () { - it('should build transfer from intent', async function () { + it('should build payment from intent', async function () { const wasmTx = buildTransaction( - { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, + { type: 'payment', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, createWasmContext() ); const serialized = wasmTx.toBroadcastFormat(); @@ -467,7 +197,7 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { assert(serialized.length > 10, 'Should have content'); }); - it('should build stake from intent', async function () { + it('should build stake (top-up) from intent', async function () { const wasmTx = buildTransaction( { type: 'stake', amount: 5000000000000n, payee: { type: 'staked' } }, createWasmContext() @@ -475,37 +205,40 @@ describe('WASM vs Legacy Builder Byte Comparison', function () { assert(wasmTx.toBroadcastFormat().startsWith('0x')); }); - it('should build withdrawUnbonded from intent', async function () { - const wasmTx = buildTransaction({ type: 'withdrawUnbonded', slashingSpans: 0 }, createWasmContext()); + it('should build stake with proxy (new stake / batchAll(bond, addProxy)) from intent', async function () { + const wasmTx = buildTransaction( + { type: 'stake', amount: 10000000000000n, proxyAddress: accounts.account2.address, payee: { type: 'staked' } }, + createWasmContext() + ); assert(wasmTx.toBroadcastFormat().startsWith('0x')); }); - it('should build chill from intent', async function () { - const wasmTx = buildTransaction({ type: 'chill' }, createWasmContext()); + it('should build claim (withdrawUnbonded) from intent', async function () { + const wasmTx = buildTransaction({ type: 'claim', slashingSpans: 0 }, createWasmContext()); assert(wasmTx.toBroadcastFormat().startsWith('0x')); }); - it('should build addProxy from intent', async function () { - const wasmTx = buildTransaction( - { type: 'addProxy', delegate: accounts.account2.address, proxyType: 'Any', delay: 0 }, - createWasmContext() - ); + it('should build unstake (partial / unbond) from intent', async function () { + const wasmTx = buildTransaction({ type: 'unstake', amount: 5000000000000n }, createWasmContext()); assert(wasmTx.toBroadcastFormat().startsWith('0x')); }); - it('should build batch from intent', async function () { + it('should build unstake with stopStaking (full / batchAll(removeProxy, chill, unbond)) from intent', async function () { const wasmTx = buildTransaction( { - type: 'batch', - calls: [ - { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, - { type: 'chill' }, - ], - atomic: true, + type: 'unstake', + amount: 5000000000000n, + stopStaking: true, + proxyAddress: accounts.account2.address, }, createWasmContext() ); assert(wasmTx.toBroadcastFormat().startsWith('0x')); }); + + it('should build consolidate (transferAll) from intent', async function () { + const wasmTx = buildTransaction({ type: 'consolidate', to: accounts.account2.address }, createWasmContext()); + assert(wasmTx.toBroadcastFormat().startsWith('0x')); + }); }); }); diff --git a/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts b/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts index ab0da5adc6..048bcc0c18 100644 --- a/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts +++ b/modules/sdk-coin-dot/test/unit/wasmParserExplanation.ts @@ -6,6 +6,10 @@ * * Uses WASM-built transactions (not legacy rawTx fixtures) since the WASM * parser requires metadata-compatible signed extension encoding. + * + * Note: wasm-dot@1.3.0 uses high-level business intents. Batch transactions + * are produced automatically from the intent (e.g., unstake with stopStaking + * produces batchAll(removeProxy, chill, unbond)). */ import assert from 'assert'; @@ -42,16 +46,13 @@ describe('WASM Parser Explanation', function () { const unbondAmount = 5000000000000n; // 5 DOT const proxyDelegate = accounts.account2.address; - // Build a batch unstake tx: removeProxy + chill + unbond + // Build a full unstake: produces batchAll(removeProxy, chill, unbond) const wasmTx = buildTransaction( { - type: 'batch', - calls: [ - { type: 'removeProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, - { type: 'chill' }, - { type: 'unstake', amount: unbondAmount }, - ], - atomic: true, + type: 'unstake', + amount: unbondAmount, + stopStaking: true, + proxyAddress: proxyDelegate, }, createWasmContext() ); @@ -90,13 +91,10 @@ describe('WASM Parser Explanation', function () { const proxyDelegate = accounts.account2.address; const wasmTx = buildTransaction( { - type: 'batch', - calls: [ - { type: 'removeProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, - { type: 'chill' }, - { type: 'unstake', amount: 1000000000000n }, - ], - atomic: true, + type: 'unstake', + amount: 1000000000000n, + stopStaking: true, + proxyAddress: proxyDelegate, }, createWasmContext() ); @@ -107,30 +105,6 @@ describe('WASM Parser Explanation', function () { assert.strictEqual(explanation1.outputs[0].amount, explanation2.outputs[0].amount); }); - - it('should work with non-atomic batch (utility.batch)', function () { - const proxyDelegate = accounts.account2.address; - const wasmTx = buildTransaction( - { - type: 'batch', - calls: [ - { type: 'removeProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, - { type: 'chill' }, - { type: 'unstake', amount: 3000000000000n }, - ], - atomic: false, - }, - createWasmContext() - ); - - const txHex = wasmTx.toBroadcastFormat(); - const explanation = explainDotTransaction({ txHex, material, senderAddress: accounts.account1.address }); - - assert.strictEqual(explanation.type, TransactionType.Batch); - assert.strictEqual(explanation.outputs.length, 1); - assert.ok(BigInt(explanation.outputs[0].amount) > 0n); - assert.strictEqual(explanation.inputs[0].address, proxyDelegate); - }); }); describe('Batch stake (bond + addProxy)', function () { @@ -138,14 +112,13 @@ describe('WASM Parser Explanation', function () { const bondAmount = 10000000000000n; // 10 DOT const proxyDelegate = accounts.account2.address; + // Build a new stake with proxy: produces batchAll(bond, addProxy) const wasmTx = buildTransaction( { - type: 'batch', - calls: [ - { type: 'stake', amount: bondAmount, payee: { type: 'staked' } }, - { type: 'addProxy', delegate: proxyDelegate, proxyType: 'Staking', delay: 0 }, - ], - atomic: true, + type: 'stake', + amount: bondAmount, + proxyAddress: proxyDelegate, + payee: { type: 'staked' }, }, createWasmContext() ); @@ -155,14 +128,18 @@ describe('WASM Parser Explanation', function () { assert.strictEqual(explanation.type, TransactionType.Batch); - // Should have two outputs: bond amount (STAKING) + proxy deposit cost (to proxy delegate) + // Should have two outputs: bond amount (to STAKING_DESTINATION sentinel) + proxy deposit cost (to proxy delegate) assert.strictEqual(explanation.outputs.length, 2, 'Should have bond + proxy deposit outputs'); - const stakingOutput = explanation.outputs.find((o) => o.address === 'STAKING'); - assert.ok(stakingOutput, 'Should have STAKING output for bond amount'); + const stakingOutput = explanation.outputs.find( + (o) => o.address === '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM' + ); + assert.ok(stakingOutput, 'Should have STAKING_DESTINATION sentinel output for bond amount'); assert.strictEqual(BigInt(stakingOutput!.amount), bondAmount, 'Bond amount should match'); - const proxyOutput = explanation.outputs.find((o) => o.address !== 'STAKING'); + const proxyOutput = explanation.outputs.find( + (o) => o.address !== '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM' + ); assert.ok(proxyOutput, 'Should have proxy deposit output'); assert.strictEqual(proxyOutput!.address, proxyDelegate); assert.ok(BigInt(proxyOutput!.amount) > 0n, 'Proxy deposit cost should be positive'); @@ -178,7 +155,7 @@ describe('WASM Parser Explanation', function () { describe('Non-batch transactions (should not be affected)', function () { it('should explain transfer normally', function () { const wasmTx = buildTransaction( - { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, + { type: 'payment', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, createWasmContext() ); @@ -205,33 +182,9 @@ describe('WASM Parser Explanation', function () { assert.strictEqual(explanation.type, TransactionType.StakingUnlock); assert.strictEqual(explanation.outputs.length, 1); - assert.strictEqual(explanation.outputs[0].address, 'STAKING'); + // STAKING_DESTINATION sentinel: unbond doesn't transfer to an external address + assert.strictEqual(explanation.outputs[0].address, '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'); assert.strictEqual(explanation.outputs[0].amount, '5000000000000'); }); - - it('should explain batch of transfers normally (no proxy involved)', function () { - const wasmTx = buildTransaction( - { - type: 'batch', - calls: [ - { type: 'transfer', to: accounts.account2.address, amount: 1000000000000n, keepAlive: true }, - { type: 'transfer', to: accounts.account3.address, amount: 2000000000000n, keepAlive: true }, - ], - atomic: true, - }, - createWasmContext() - ); - - const explanation = explainDotTransaction({ - txHex: wasmTx.toBroadcastFormat(), - material, - senderAddress: accounts.account1.address, - }); - - assert.strictEqual(explanation.type, TransactionType.Batch); - assert.strictEqual(explanation.outputs.length, 2); - assert.strictEqual(explanation.outputs[0].amount, '1000000000000'); - assert.strictEqual(explanation.outputs[1].amount, '2000000000000'); - }); }); }); diff --git a/yarn.lock b/yarn.lock index 330fc26f77..113d7b4ced 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,7 +45,7 @@ "@api-ts/openapi-generator@5.14.0": version "5.14.0" - resolved "https://registry.npmjs.org/@api-ts/openapi-generator/-/openapi-generator-5.14.0.tgz#13d8370ad04fa5b12d49e7f07651af216e4f7331" + resolved "https://registry.npmjs.org/@api-ts/openapi-generator/-/openapi-generator-5.14.0.tgz" integrity sha512-adpM9cRCkprZPawF7rcWL230S5pcGUnumsQaYonkmsIOEcYn7l6/qvtJI7ZXLFt3lqyH9ifPg3eBUk6nsyR2wA== dependencies: "@swc/core" "1.5.7" @@ -124,6 +124,15 @@ js-tokens "^4.0.0" picocolors "^1.1.1" +"@babel/code-frame@^7.28.6": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0": version "7.28.0" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" @@ -310,12 +319,12 @@ "@babel/types" "^7.28.2" "@babel/helpers@^7.28.2", "@babel/helpers@^7.28.3": - version "7.28.4" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" - integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== dependencies: - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.4" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" "@babel/highlight@^7.10.4": version "7.25.9" @@ -334,6 +343,13 @@ dependencies: "@babel/types" "^7.28.2" +"@babel/parser@^7.28.6": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.27.1" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz" @@ -890,9 +906,9 @@ esutils "^2.0.2" "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.25.0", "@babel/runtime@^7.28.2", "@babel/runtime@^7.28.6", "@babel/runtime@^7.7.6": - version "7.28.4" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" - integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== "@babel/template@^7.27.1", "@babel/template@^7.27.2": version "7.27.2" @@ -903,6 +919,15 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + "@babel/traverse@^7.23.2", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.4.5": version "7.28.3" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz" @@ -924,10 +949,10 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" -"@babel/types@^7.28.4": - version "7.28.5" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz" - integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== +"@babel/types@^7.28.6", "@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" @@ -976,7 +1001,7 @@ "@bitgo/public-types@5.76.1": version "5.76.1" - resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-5.76.1.tgz#c36b245fccc4a90068fced8a4e2985d57f561bc1" + resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-5.76.1.tgz" integrity sha512-S3dKa1to6xQj/cmtKrip6ytG1/4qBkRhZ117cOERlRcHHJFY8/h+1zCKazlRU+FRc8JUWcLlAoFnAcqifHw3Eg== dependencies: fp-ts "^2.0.0" @@ -985,20 +1010,25 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/wasm-dot@^1.1.1", "@bitgo/wasm-dot@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.1.2.tgz#af1b190ea684838a512e47f4036c453285229bb2" - integrity sha512-zVGsDG+eFpaEnlVbpniSB8ANms/BQgl54pMcsaPJmzrCwpN56CxEWdYyvsvfdNuOC6Le3zvx/6KF75RdmFxnCA== +"@bitgo/wasm-dot@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.2.0.tgz" + integrity sha512-YOqNydELWEvkK6WAgrQDfXhQNgE6rgWVaku2DYONdo5XfPCjjki/G/h4zzirE5bH9/KxWmU1XN6OSshoBgSpAA== + +"@bitgo/wasm-dot@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.3.0.tgz#ab3049ee96c267ed4a2aec0034fb7eb1e6bafa3e" + integrity sha512-9DX4ozXkS+MvrP1IrhGpSQhB9a0by/EtRl2UIZrBmbAV8O0BNXfEfeFvl71PZC4GmHLj7CKU1coDPay+nwg12Q== "@bitgo/wasm-solana@^2.6.0": version "2.6.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-solana/-/wasm-solana-2.6.0.tgz#c8b57ab010f22f1a1c90681cd180814c4ec2867b" + resolved "https://registry.npmjs.org/@bitgo/wasm-solana/-/wasm-solana-2.6.0.tgz" integrity sha512-F9H4pXDMhfsZW5gNEcoaBzVoEMOQRP8wbQKmjsxbm5PXBq+0Aj54rOY3bswdrFZK377/aeB+tLjXu3h9i8gInQ== "@bitgo/wasm-utxo@^1.42.0": - version "1.42.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.42.0.tgz#29754e9b947fd9d5c303cb5f5043a031cd04754b" - integrity sha512-VKhlaSVjD7dTjYw5yNbRVTQh84zuyiEcLSldhzbnkrWgZL+3+xYaOAWicVJaE5Bk37AYWRL+7aiwbCtosF7zug== + version "1.44.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.44.0.tgz" + integrity sha512-fJC/W/lpjT/21v7NevZnQHf84185xONBc1SjOr+ie5C7u+djY4sFOGG8l0PF6GTARsN5i6FgYJfDLuhp+yVkcw== "@brandonblack/musig@^0.0.1-alpha.0": version "0.0.1-alpha.1" @@ -1042,7 +1072,7 @@ "@clack/core@^0.3.3": version "0.3.5" - resolved "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz#3e1454c83a329353cc3a6ff8491e4284d49565bb" + resolved "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz" integrity sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ== dependencies: picocolors "^1.0.0" @@ -1050,7 +1080,7 @@ "@clack/prompts@^0.7.0": version "0.7.0" - resolved "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz#6aaef48ea803d91cce12bc80811cfcb8de2e75ea" + resolved "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz" integrity sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA== dependencies: "@clack/core" "^0.3.3" @@ -2747,7 +2777,7 @@ "@flarenetwork/flarejs@4.1.1": version "4.1.1" - resolved "https://registry.npmjs.org/@flarenetwork/flarejs/-/flarejs-4.1.1.tgz#5aac35a43431e9e08263094da48838e6159a69e7" + resolved "https://registry.npmjs.org/@flarenetwork/flarejs/-/flarejs-4.1.1.tgz" integrity sha512-XuzMROKI/4LfOWt2NY3suagmq0PjRbhyVaDznfVzTI0kRl/64xDc74kElusidewh55Y/5Ajrl1wBPrRhXG4fNQ== dependencies: "@noble/curves" "1.3.0" @@ -3976,49 +4006,49 @@ integrity sha512-Iwf7gxWMeDdrNqXYkVOib6PlDYwLw51+nMiFm1UW5nKxbQyVYHp7lhQNHsZrQ7Oqo84m9swWgzE7bhs21HkbYQ== "@nx/nx-darwin-x64@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.0.4.tgz#7b501c1556cbfd7f88c8655d5a2c5fa4b237b8e1" - integrity sha512-p+pmlq/mdNhQb12RwHP9V6yAUX9CLy8GUT4ijPzFTbxqa9dZbJk69NpSRwpAhAvvQ30gp1Zyh0t0/k/yaZqMIg== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.5.4.tgz#3280d0905c53631299869629e88611c0f012b690" + integrity sha512-DjyXuQMc93MPU2XdRsJYjzbv1tgCzMi+zm7O0gc4x3h+ECFjKkjzQBg67pqGdhE3TV27MAlVRKrgHStyK9iigg== "@nx/nx-freebsd-x64@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.0.4.tgz#6d9ba7748d406b0914985945eeb31adf9d382956" - integrity sha512-XW2SXtfO245DRnAXVGYJUB7aBJsJ2rPD5pizxJET+l3VmtHGp2crdVuftw6iqjgrf2eAS+yCe61Jnqh687vWFg== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.5.4.tgz#65c79238ca8e726c18296192491d7e72dbbbabe5" + integrity sha512-DhxdP8AhIfN0yCtFhZQcbp32MVN3L7UiTotYqqnOgwW922NRGSd5e+KEAWiJVrIO6TdgnI7prxpg1hfQQK0WDw== "@nx/nx-linux-arm-gnueabihf@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.0.4.tgz#363c8adf6b4d836709a4d54f6263f113f1039af7" - integrity sha512-LCLuhbW3SIFz2FGiLdspCrNP889morCzTV/pEtxA8EgusWqCR8WjeSj3QvN8HN/GoXDsJxoUXvClZbHE+N6Hyg== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.5.4.tgz#e13acda0ba22b0ec9d530923bd7eeec73f6a0fd0" + integrity sha512-pv1x1afTaLAOxPxVhQneLeXgjclp11f9ORxR7jA4E86bSgc9OL92dLSCkXtLQzqPNOej6SZ2fO+PPHVMZwtaPQ== "@nx/nx-linux-arm64-gnu@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.0.4.tgz#81d9470130742f7042d870e584bb7728ed3cbc44" - integrity sha512-2jvS8MYYOI8eUBRTmE8HKm5mRVLqS5Cvlj06tEAjxrmH5d7Bv8BG5Ps9yZzT0qswfVKChpzIliwPZomUjLTxmA== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.5.4.tgz#837c64bd477b95150ee309c30cbdba640ef65803" + integrity sha512-mPji9PzleWPvXpmFDKaXpTymRgZkk/hW8JHGhvEZpKHHXMYgTGWC+BqOEM2A4dYC4bu4fi9RrteL7aouRRWJoQ== "@nx/nx-linux-arm64-musl@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.0.4.tgz#7f8176cf76ef7fc81af4ef434ac3d3959dcbf272" - integrity sha512-IK9gf8/AOtTW6rZajmGAFCN7EBzjmkIevt9MtOehQGlNXlMXydvUYKE5VU7d4oglvYs8aJJyayihfiZbFnTS8g== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.5.4.tgz#7e762b33ff16441cfc9d77e6d8858fde3e8d5c28" + integrity sha512-hF/HvEhbCjcFpTgY7RbP1tUTbp0M1adZq4ckyW8mwhDWQ/MDsc8FnOHwCO3Bzy9ZeJM0zQUES6/m0Onz8geaEA== "@nx/nx-linux-x64-gnu@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.0.4.tgz#faeb8d6f54d81dd73abc21a2928cf4c18b810cc6" - integrity sha512-CdALjMqqNgiffQQIlyxx6mrxJCOqDzmN6BW3w9msCPHVSPOPp4AenlT0kpC7ALvmNEUm0lC4r093QbN2t6a/wA== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.5.4.tgz#ce33bc0b56914860ec7f044370b28cadb96c2d50" + integrity sha512-1+vicSYEOtc7CNMoRCjo59no4gFe8w2nGIT127wk1yeW3EJzRVNlOA7Deu10NUUbzLeOvHc8EFOaU7clT+F7XQ== "@nx/nx-linux-x64-musl@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.0.4.tgz#2cc0e5be5415ac85f164943ea42ca4ca0889bcf9" - integrity sha512-2GPy+mAQo4JnfjTtsgGrHhZbTmmGy4RqaGowe0qMYCMuBME33ChG9iiRmArYmVtCAhYZVn26rK76/Vn3tK7fgg== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.5.4.tgz#7d2d045ee69aea5d84235beab8b1217880de12d1" + integrity sha512-/KjndxVB14yU0SJOhqADHOWoTy4Y45h5RjW3cxcXlPSJZz7ar1FnlLne1rWMMMUttepc8ku+3T//SGKi2eu+Nw== "@nx/nx-win32-arm64-msvc@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.0.4.tgz#796e81bef402f216fa170eddd912578acb7bd939" - integrity sha512-jnZCCnTXoqOIrH0L31+qHVHmJuDYPoN6sl37/S1epP9n4fhcy9tjSx4xvx/WQSd417lU9saC+g7Glx2uFdgcTw== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.5.4.tgz#58144fdc59b9c4653c623e8537bb2b5ea2e5a7e0" + integrity sha512-CrYt9FwhjOI6ZNy/G6YHLJmZuXCFJ24BCxugPXiZ7knDx7eGrr7owGgfht4SSiK3KCX40CvWCBJfqR4ZSgaSUA== "@nx/nx-win32-x64-msvc@*": - version "22.0.4" - resolved "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.0.4.tgz#958951dd96ee14d908674f2846a9dc0f85318618" - integrity sha512-CDBqgb9RV5aHMDLcsS9kDDULc38u/eieZBhHBL01Ca5Tq075QuHn4uly6sYyHwVOxrhY4eaWNSfV2xG3Bg6Gtw== + version "22.5.4" + resolved "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.5.4.tgz#87ae9a4f0d77b63f9e067be50f1dbf15749e9575" + integrity sha512-g5YByv4XsYwsYZvFe24A9bvfhZA+mwtIQt6qZtEVduZTT1hfhIsq0LXGHhkGoFLYwRMXSracWOqkalY0KT4IQw== "@octokit/auth-token@^2.4.4": version "2.5.0" @@ -4403,7 +4433,7 @@ "@polkadot-api/substrate-client@^0.1.2": version "0.1.4" - resolved "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.1.4.tgz" + resolved "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.1.4.tgz#7a808e5cb85ecb9fa2b3a43945090a6c807430ce" integrity sha512-MljrPobN0ZWTpn++da9vOvt+Ex+NlqTlr/XT7zi9sqPtDJiQcYl+d29hFAgpaeTqbeQKZwz3WDE9xcEfLE8c5A== dependencies: "@polkadot-api/json-rpc-provider" "0.0.1" @@ -4479,7 +4509,7 @@ "@polkadot/keyring@13.5.6", "@polkadot/keyring@^13.1.1", "@polkadot/keyring@^13.2.1": version "13.5.6" - resolved "https://registry.npmjs.org/@polkadot/keyring/-/keyring-13.5.6.tgz#b26d0cba323bb0520826211317701aa540428406" + resolved "https://registry.npmjs.org/@polkadot/keyring/-/keyring-13.5.6.tgz" integrity sha512-Ybe6Mflrh96FKR5tfEaf/93RxJD7x9UigseNOJW6Yd8LF+GesdxrqmZD7zh+53Hb7smGQWf/0FCfwhoWZVgPUQ== dependencies: "@polkadot/util" "13.5.6" @@ -4799,7 +4829,7 @@ "@puppeteer/browsers@2.6.1": version "2.6.1" - resolved "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz#d75aec5010cae377c5e4742bf5e4f62a79c21315" + resolved "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz" integrity sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg== dependencies: debug "^4.4.0" @@ -5558,7 +5588,7 @@ "@swc/core-darwin-arm64@1.5.7": version "1.5.7" - resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz#2b5cdbd34e4162e50de6147dd1a5cb12d23b08e8" + resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz" integrity sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ== "@swc/core-darwin-x64@1.5.7": @@ -5583,7 +5613,7 @@ "@swc/core-linux-x64-gnu@1.5.7": version "1.5.7" - resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz#a699c1632de60b6a63b7fdb7abcb4fef317e57ca" + resolved "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz" integrity sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg== "@swc/core-linux-x64-musl@1.5.7": @@ -5608,7 +5638,7 @@ "@swc/core@1.5.7": version "1.5.7" - resolved "https://registry.npmjs.org/@swc/core/-/core-1.5.7.tgz#e1db7b9887d5f34eb4a3256a738d0c5f1b018c33" + resolved "https://registry.npmjs.org/@swc/core/-/core-1.5.7.tgz" integrity sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ== dependencies: "@swc/counter" "^0.1.2" @@ -5627,7 +5657,7 @@ "@swc/counter@^0.1.2", "@swc/counter@^0.1.3": version "0.1.3" - resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== "@swc/helpers@^0.5.11": @@ -5639,7 +5669,7 @@ "@swc/types@0.1.7": version "0.1.7" - resolved "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz#ea5d658cf460abff51507ca8d26e2d391bafb15e" + resolved "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz" integrity sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ== dependencies: "@swc/counter" "^0.1.3" @@ -6200,12 +6230,12 @@ dependencies: "@types/node" "*" -"@types/node@*": - version "22.18.0" - resolved "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz" - integrity sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ== +"@types/node@*", "@types/node@>= 8", "@types/node@>=10.0.0", "@types/node@>=13.7.0", "@types/node@^24.10.9": + version "24.10.9" + resolved "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz" + integrity sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw== dependencies: - undici-types "~6.21.0" + undici-types "~7.16.0" "@types/node@11.11.6": version "11.11.6" @@ -6219,20 +6249,6 @@ dependencies: undici-types "~6.19.2" -"@types/node@>= 8", "@types/node@>=13.7.0": - version "24.3.0" - resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz" - integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== - dependencies: - undici-types "~7.10.0" - -"@types/node@>=10.0.0": - version "24.3.1" - resolved "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz" - integrity sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g== - dependencies: - undici-types "~7.10.0" - "@types/node@^12.12.54", "@types/node@^12.12.7": version "12.20.55" resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" @@ -6250,13 +6266,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@^24.10.9": - version "24.10.9" - resolved "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz#1aeb5142e4a92957489cac12b07f9c7fe26057d0" - integrity sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw== - dependencies: - undici-types "~7.16.0" - "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" @@ -6555,20 +6564,13 @@ resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== -"@types/yargs@17.0.19": +"@types/yargs@17.0.19", "@types/yargs@^17.0.0": version "17.0.19" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.19.tgz" integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.0": - version "17.0.35" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" - integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== - dependencies: - "@types/yargs-parser" "*" - "@types/yauzl@^2.9.1": version "2.10.3" resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz" @@ -7554,7 +7556,7 @@ aws4@^1.8.0: axios@0.25.0, axios@0.27.2, axios@1.7.4, axios@^0.21.2, axios@^0.26.1, axios@^1.13.0, axios@^1.6.0, axios@^1.8.3: version "1.13.5" - resolved "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43" + resolved "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz" integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q== dependencies: follow-redirects "^1.15.11" @@ -7563,7 +7565,7 @@ axios@0.25.0, axios@0.27.2, axios@1.7.4, axios@^0.21.2, axios@^0.26.1, axios@^1. b4a@^1.6.4: version "1.7.3" - resolved "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz#24cf7ccda28f5465b66aec2bac69e32809bf112f" + resolved "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz" integrity sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q== b64-lite@^1.3.1, b64-lite@^1.4.0: @@ -7630,12 +7632,12 @@ balanced-match@^1.0.0: bare-events@^2.5.4, bare-events@^2.7.0: version "2.8.2" - resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz#7b3e10bd8e1fc80daf38bb516921678f566ab89f" + resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz" integrity sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ== bare-fs@^4.0.1: version "4.5.1" - resolved "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.1.tgz#498a20a332d4a7f0b310eb89b8d2319041aa1eef" + resolved "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.1.tgz" integrity sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg== dependencies: bare-events "^2.5.4" @@ -7646,26 +7648,26 @@ bare-fs@^4.0.1: bare-os@^3.0.1: version "3.6.2" - resolved "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz#b3c4f5ad5e322c0fd0f3c29fc97d19009e2796e5" + resolved "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz" integrity sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A== bare-path@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz#b59d18130ba52a6af9276db3e96a2e3d3ea52178" + resolved "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz" integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw== dependencies: bare-os "^3.0.1" bare-stream@^2.6.4: version "2.7.0" - resolved "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz#5b9e7dd0a354d06e82d6460c426728536c35d789" + resolved "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz" integrity sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A== dependencies: streamx "^2.21.0" bare-url@^2.2.2: version "2.3.2" - resolved "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz#4aef382efa662b2180a6fe4ca07a71b39bdf7ca3" + resolved "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz" integrity sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw== dependencies: bare-path "^3.0.0" @@ -8334,9 +8336,9 @@ buffer@4.9.2, buffer@6.0.3, buffer@^5.0.2, buffer@^5.1.0, buffer@^5.2.1, buffer@ ieee754 "^1.2.1" bufferutil@^4.0.1: - version "4.0.9" - resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz" - integrity sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== + version "4.1.0" + resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz#a4623541dd23867626bb08a051ec0d2ec0b70294" + integrity sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw== dependencies: node-gyp-build "^4.3.0" @@ -8755,7 +8757,7 @@ chrome-trace-event@^1.0.2: chromium-bidi@0.11.0: version "0.11.0" - resolved "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz#9c3c42ee7b42d8448e9fce8d649dc8bfbcc31153" + resolved "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz" integrity sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA== dependencies: mitt "3.0.1" @@ -8921,7 +8923,7 @@ cmd-shim@^7.0.0: cmd-ts@0.13.0: version "0.13.0" - resolved "https://registry.npmjs.org/cmd-ts/-/cmd-ts-0.13.0.tgz#57bdbc5dc95eb5a3503ab3ac9591c91427a79fa1" + resolved "https://registry.npmjs.org/cmd-ts/-/cmd-ts-0.13.0.tgz" integrity sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g== dependencies: chalk "^4.0.0" @@ -9797,7 +9799,7 @@ debug@^3.1.0, debug@^3.2.7: debug@^4.4.0: version "4.4.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" @@ -10092,7 +10094,7 @@ detective@^4.0.0: devtools-protocol@0.0.1367902: version "0.0.1367902" - resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz#7333bfc4466c5a54a4c6de48a9dfbcb4b811660c" + resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz" integrity sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg== dezalgo@^1.0.4: @@ -10110,7 +10112,7 @@ di@^0.0.1: didyoumean@^1.2.2: version "1.2.2" - resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== diff@^3.5.0: @@ -10241,7 +10243,7 @@ domhandler@^5.0.2, domhandler@^5.0.3: dompurify@^3.3.1: version "3.3.1" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz#c7e1ddebfe3301eacd6c0c12a4af284936dbbb86" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz" integrity sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -10439,7 +10441,7 @@ encodeurl@~2.0.0: encoding@^0.1.13: version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: iconv-lite "^0.6.2" @@ -11259,7 +11261,7 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.4: events-universal@^1.0.0: version "1.0.1" - resolved "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz#b56a84fd611b6610e0a2d0f09f80fdf931e2dfe6" + resolved "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz" integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw== dependencies: bare-events "^2.7.0" @@ -11493,7 +11495,7 @@ fast-diff@^1.1.2: fast-fifo@^1.2.0, fast-fifo@^1.3.2: version "1.3.2" - resolved "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + resolved "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== fast-glob@^3.2.5, fast-glob@^3.2.9: @@ -11876,7 +11878,7 @@ fp-ts@2.16.2: fp-ts@2.16.9: version "2.16.9" - resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz#99628fc5e0bb3b432c4a16d8f4455247380bae8a" + resolved "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz" integrity sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ== fp-ts@^2.0.0, fp-ts@^2.12.2, fp-ts@^2.16.2: @@ -12883,7 +12885,7 @@ iconv-lite@0.4.24: iconv-lite@^0.6.2: version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" @@ -14005,7 +14007,7 @@ jsonpointer@^5.0.0: jspdf@>=4.2.0: version "4.2.0" - resolved "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz#f5b42a8e1592c3da1531d005adc87ccc19272965" + resolved "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz" integrity sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q== dependencies: "@babel/runtime" "^7.28.6" @@ -15268,7 +15270,7 @@ minizlib@^3.0.1, minizlib@^3.1.0: mitt@3.0.1: version "3.0.1" - resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== mkdirp@^0.5.5: @@ -16204,7 +16206,7 @@ open@^8.4.0: openapi-types@12.1.3: version "12.1.3" - resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== opener@^1.5.2: @@ -17459,7 +17461,7 @@ proxy-agent@6.4.0: proxy-agent@^6.5.0: version "6.5.0" - resolved "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d" + resolved "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz" integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A== dependencies: agent-base "^7.1.2" @@ -17527,7 +17529,7 @@ punycode@^2.1.0, punycode@^2.1.1: puppeteer-core@23.11.1: version "23.11.1" - resolved "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz#3e064de11b3cb3a2df1a8060ff2d05b41be583db" + resolved "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz" integrity sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg== dependencies: "@puppeteer/browsers" "2.6.1" @@ -17539,7 +17541,7 @@ puppeteer-core@23.11.1: puppeteer@^23.10.0: version "23.11.1" - resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz#98fd9040786b1219b1a4f639c270377586e8899c" + resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz" integrity sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw== dependencies: "@puppeteer/browsers" "2.6.1" @@ -18504,7 +18506,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: semver@^7.6.3: version "7.7.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== send@0.19.0: @@ -18884,7 +18886,7 @@ sirv@^2.0.3: sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== sjcl@1.0.8, sjcl@^1.0.6: @@ -18930,7 +18932,7 @@ smart-buffer@^4.1.0, smart-buffer@^4.2.0: smoldot@2.0.26: version "2.0.26" - resolved "https://registry.npmjs.org/smoldot/-/smoldot-2.0.26.tgz" + resolved "https://registry.npmjs.org/smoldot/-/smoldot-2.0.26.tgz#0e64c7fcd26240fbe4c8d6b6e4b9a9aca77e00f6" integrity sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig== dependencies: ws "^8.8.1" @@ -19344,7 +19346,7 @@ streamroller@^3.1.5: streamx@^2.15.0, streamx@^2.21.0: version "2.23.0" - resolved "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz#7d0f3d00d4a6c5de5728aecd6422b4008d66fd0b" + resolved "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz" integrity sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg== dependencies: events-universal "^1.0.0" @@ -19735,7 +19737,7 @@ tape@^4.6.3: tar-fs@^3.0.6: version "3.1.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz#4f164e59fb60f103d472360731e8c6bb4a7fe9ef" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz" integrity sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg== dependencies: pump "^3.0.0" @@ -19746,7 +19748,7 @@ tar-fs@^3.0.6: tar-stream@^3.1.5: version "3.1.7" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz" integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== dependencies: b4a "^1.6.4" @@ -19766,7 +19768,7 @@ tar-stream@~2.2.0: tar@6.2.1, tar@^6.1.11, tar@^6.1.2: version "6.2.1" - resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" @@ -19829,7 +19831,7 @@ test-exclude@^6.0.0: text-decoder@^1.1.0: version "1.2.3" - resolved "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + resolved "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz" integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== dependencies: b4a "^1.6.4" @@ -20307,7 +20309,7 @@ typed-array-length@^1.0.7: typed-query-selector@^2.12.0: version "2.12.0" - resolved "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + resolved "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz" integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== typedarray-to-buffer@^3.1.5: @@ -20405,7 +20407,7 @@ unbox-primitive@^1.1.0: unbzip2-stream@^1.4.3: version "1.4.3" - resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + resolved "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz" integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== dependencies: buffer "^5.2.1" @@ -20432,19 +20434,9 @@ undici-types@~6.19.2: resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== -undici-types@~6.21.0: - version "6.21.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" - integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== - -undici-types@~7.10.0: - version "7.10.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz" - integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== - undici-types@~7.16.0: version "7.16.0" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== unicode-canonical-property-names-ecmascript@^2.0.0: @@ -20602,7 +20594,7 @@ use-latest@^1.2.1: utf-8-validate@^5.0.2: version "5.0.10" - resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz" + resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== dependencies: node-gyp-build "^4.3.0" @@ -21288,7 +21280,7 @@ ws@5.2.4: dependencies: async-limiter "~1.0.0" -ws@7.4.6, ws@8.18.3, ws@8.8.0, ws@^8.13.0, ws@^8.18.0, ws@^8.5.0, ws@^8.8.1: +ws@7.4.6, ws@8.18.3, ws@8.8.0, ws@^8.13.0, ws@^8.18.0, ws@^8.5.0: version "8.18.3" resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== @@ -21298,9 +21290,14 @@ ws@7.5.10, ws@8.17.1, ws@8.18.0, ws@^7, ws@^7.0.0, ws@^7.3.1, ws@^7.5.10: resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@^8.8.1: + version "8.19.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz#ddc2bdfa5b9ad860204f5a72a4863a8895fd8c8b" + integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg== + ws@~8.17.1: version "8.17.1" - resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== wsl-utils@^0.1.0: @@ -21511,7 +21508,7 @@ yoctocolors-cjs@^2.1.2: zod@3.23.8: version "3.23.8" - resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== zod@^3.21.4: