diff --git a/.github/prompts/code-review.md b/.github/prompts/code-review.md index 9ca6fb279c..4651cfd505 100644 --- a/.github/prompts/code-review.md +++ b/.github/prompts/code-review.md @@ -8,6 +8,15 @@ You are an expert code reviewer for the BitGoJS cryptocurrency wallet SDK. Pleas - Proper validation of transaction parameters - Safe handling of private keys and sensitive data +## Internal Information Leakage (Public Repository) +Comments and strings should describe what the code does, not the dev process. Flag in comments, JSDoc, test names, and error/log strings: +- Verification/testing metadata (dates, "dry-run confirmed", "verified/tested on", investigation notes) +- Internal team/system names or codenames (e.g. "by WP"), infra, or tooling +- Internal ticket IDs or links to internal-only docs +- Rationale on how/why a change was made rather than code behavior + +For each, suggest a behavior-only rewrite. + ## Code Quality & Architecture - Adherence to BitGoJS coding standards and patterns - TypeScript type safety and interface compliance @@ -35,8 +44,9 @@ You are an expert code reviewer for the BitGoJS cryptocurrency wallet SDK. Pleas Please provide constructive feedback focusing on: 1. Critical issues that must be addressed -2. Suggestions for improvement -3. Questions about design decisions -4. Acknowledgment of good practices +2. Internal-information leaks in comments or strings (must be removed before merge) +3. Suggestions for improvement +4. Questions about design decisions +5. Acknowledgment of good practices Be thorough but concise, and explain the reasoning behind your suggestions. diff --git a/.github/workflows/npmjs-release.yml b/.github/workflows/npmjs-release.yml index 59054057f1..03f9477087 100644 --- a/.github/workflows/npmjs-release.yml +++ b/.github/workflows/npmjs-release.yml @@ -236,9 +236,13 @@ jobs: run: | yarn install --frozen-lockfile + - name: Install retry + uses: BitGo/install-github-release-binary@v2 + with: + targets: EricCrosson/retry@v1.4.8:sha256-d207746ff0eda67c706df25e88c02520f0cf3172279eb8eec8224fb0d3558911 + - name: Run yarn audit - run: | - yarn run audit-high + run: retry --up-to 2x --every 3s -- yarn run audit-high --retry-on-network-failure - name: Run dependency check run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f6de2eeeac..132104e1bf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,8 +36,13 @@ jobs: - name: Install BitGoJS run: sfw yarn install --with-frozen-lockfile + - name: Install retry + uses: BitGo/install-github-release-binary@v2 + with: + targets: EricCrosson/retry@v1.4.8:sha256-d207746ff0eda67c706df25e88c02520f0cf3172279eb8eec8224fb0d3558911 + - name: Audit Dependencies - run: yarn run improved-yarn-audit --min-severity high + run: retry --up-to 2x --every 3s -- yarn run improved-yarn-audit --min-severity high --retry-on-network-failure - name: Set Environment Variable for Alpha if: github.ref != 'refs/heads/master' # only publish changes if on feature branches diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index 64d4a03455..7ef119ee93 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -78,6 +78,7 @@ }, "devDependencies": { "@bitgo/sdk-test": "^9.1.50", + "@bitgo/utxo-lib": "^11.24.0", "mocha": "^10.2.0" }, "gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c" diff --git a/modules/abstract-utxo/src/abstractUtxoCoin.ts b/modules/abstract-utxo/src/abstractUtxoCoin.ts index 6eaaf840cf..4ebe72d4c3 100644 --- a/modules/abstract-utxo/src/abstractUtxoCoin.ts +++ b/modules/abstract-utxo/src/abstractUtxoCoin.ts @@ -2,9 +2,7 @@ import assert from 'assert'; import { randomBytes } from 'crypto'; import _ from 'lodash'; -import * as utxolib from '@bitgo/utxo-lib'; -import { BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; -import { bitgo, getMainnet } from '@bitgo/utxo-lib'; +import { address as wasmAddress, BIP32, fixedScriptWallet, hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AddressCoinSpecific, BaseCoin, @@ -62,7 +60,6 @@ import { getReplayProtectionPubkeys, isReplayProtectionUnspent } from './transac import { supportedCrossChainRecoveries } from './config'; import { assertValidTransactionRecipient, - DecodedTransaction, explainTx, fromExtendedAddressFormat, isScriptRecipient, @@ -77,14 +74,7 @@ import { ErrorImplicitExternalOutputs, } from './transaction/descriptor/verifyTransaction'; import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor'; -import { - getFullNameFromCoinName, - getMainnetCoinName, - getNetworkFromCoinName, - isMainnetCoin, - UtxoCoinName, - UtxoCoinNameMainnet, -} from './names'; +import { getFullNameFromCoinName, getMainnetCoinName, isMainnetCoin, UtxoCoinName, UtxoCoinNameMainnet } from './names'; import { assertFixedScriptWalletAddress } from './address/fixedScript'; import { ParsedTransaction } from './transaction/types'; import { decodeDescriptorPsbt, decodePsbt, encodeTransaction, stringToBufferTryFormats } from './transaction/decode'; @@ -96,7 +86,7 @@ import { isUtxoWalletData, UtxoWallet } from './wallet'; import { isDescriptorWalletData } from './descriptor/descriptorWallet'; import type { Unspent } from './unspent'; -import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; +type ScriptType2Of3 = 'p2sh' | 'p2shP2wsh' | 'p2wsh' | 'p2tr' | 'p2trMusig2'; export type TxFormat = // This is a legacy transaction format based around the bitcoinjs-lib serialization of unsigned transactions @@ -142,28 +132,19 @@ type UtxoCustomSigningFunction = { }): Promise; }; -const { isChainCode, scriptTypeForChain, outputScripts } = bitgo; - /** * Check if a decoded transaction has at least one taproot key path spend (MuSig2) input. - * Works for both utxolib UtxoPsbt and wasm-utxo BitGoPsbt. */ -function hasKeyPathSpendInput( - tx: DecodedTransaction, +function hasKeyPathSpendInput( + tx: fixedScriptWallet.BitGoPsbt, pubs: string[] | undefined, coinName: UtxoCoinName ): boolean { - if (tx instanceof bitgo.UtxoPsbt) { - return bitgo.isTransactionWithKeyPathSpendInput(tx); - } - if (tx instanceof fixedScriptWallet.BitGoPsbt) { - assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); - const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); - const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; - const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); - return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); - } - return false; + assert(pubs && isTriple(pubs), 'pub triple is required to check for key path spend inputs in wasm-utxo PSBT'); + const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(pubs); + const replayProtection = { publicKeys: getReplayProtectionPubkeys(coinName) }; + const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, { replayProtection }); + return parsed.inputs.some((input) => input.scriptType === 'p2trMusig2KeyPath'); } /** @@ -216,8 +197,6 @@ function convertValidationErrorToTxIntentMismatch( export type { DecodedTransaction } from './transaction/types'; -export type RootWalletKeys = bitgo.RootWalletKeys; - export type UtxoCoinSpecific = AddressCoinSpecific | DescriptorAddressCoinSpecific; export interface VerifyAddressOptions extends BaseVerifyAddressOptions { @@ -252,8 +231,6 @@ export interface DecoratedExplainTransactionOptions extends BaseTransactionPrebuild { txInfo?: TransactionInfo; blockHeight?: number; @@ -335,11 +312,6 @@ type UtxoBaseSignTransactionOptions = * When false, creates half-signed transaction with placeholder signatures. */ isLastSignature?: boolean; - /** - * If true, allows signing a non-segwit input with a witnessUtxo instead requiring a previous - * transaction (nonWitnessUtxo) - */ - allowNonSegwitSigningWithoutPrevTx?: boolean; /** * When true, the signed transaction will be converted from PSBT to legacy format before returning. * Set automatically by presignTransaction() when the caller explicitly requested txFormat: 'legacy'. @@ -432,14 +404,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici this.amountType = amountType; } - /** - * @deprecated - will be removed when we drop support for utxolib - * Use `name` property instead. - */ - get network(): utxolib.Network { - return getNetworkFromCoinName(this.name); - } - getChain(): UtxoCoinName { return this.name; } @@ -455,13 +419,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici /** Indicates whether the coin supports a block target */ supportsBlockTarget(): boolean { // FIXME: the SDK does not seem to use this anywhere so it is unclear what the purpose of this method is - switch (getMainnet(this.network)) { - case utxolib.networks.bitcoin: - case utxolib.networks.dogecoin: - return true; - default: - return false; - } + const mainnet = getMainnetCoinName(this.name); + return mainnet === 'btc' || mainnet === 'doge'; } sweepWithSendMany(): boolean { @@ -470,7 +429,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici /** @deprecated */ static get validAddressTypes(): ScriptType2Of3[] { - return [...outputScripts.scriptTypes2Of3]; + return ['p2sh', 'p2shP2wsh', 'p2wsh', 'p2tr', 'p2trMusig2']; } /** @@ -508,15 +467,20 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici // At the time of writing, the only additional address format is bch cashaddr. const anyFormat = (param as { anyFormat: boolean } | undefined)?.anyFormat ?? true; try { - // Find out if the address is valid for any format. Tries all supported formats by default. - // Throws if address cannot be decoded with any format. - const [format, script] = utxolib.addressFormat.toOutputScriptAndFormat(address, this.network); - // unless anyFormat is set, only 'default' is allowed. - if (!anyFormat && format !== 'default') { - return false; + const script = wasmAddress.toOutputScriptWithCoin(address, this.name); + // Determine which format the input address was in by round-tripping + // through each candidate and checking byte-equality. 'default' is tried + // first so canonical default-format addresses early-exit. + for (const format of ['default', 'cashaddr'] as const) { + try { + if (wasmAddress.fromOutputScriptWithCoin(script, this.name, format) === address) { + return anyFormat || format === 'default'; + } + } catch { + // coin doesn't support this format; try the next one + } } - // make sure that address is in normal representation for given format. - return address === utxolib.addressFormat.fromOutputScriptWithFormat(script, format, this.network); + return false; } catch (e) { return false; } @@ -596,13 +560,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @param addressDetails */ static inferAddressType(addressDetails: { chain: number }): ScriptType2Of3 | null { - return isChainCode(addressDetails.chain) ? scriptTypeForChain(addressDetails.chain) : null; - } - - createTransactionFromHex( - hex: string - ): utxolib.bitgo.UtxoTransaction { - return utxolib.bitgo.createTransactionFromHex(hex, this.network, this.amountType); + return fixedScriptWallet.ChainCode.is(addressDetails.chain) + ? (fixedScriptWallet.ChainCode.scriptType(addressDetails.chain) as ScriptType2Of3) + : null; } decodeTransaction(input: Buffer | string): fixedScriptWallet.BitGoPsbt { @@ -761,7 +721,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @returns true iff coin supports spending from unspentType */ supportsAddressType(addressType: ScriptType2Of3): boolean { - return utxolib.bitgo.outputScripts.isSupportedScriptType(this.network, addressType); + return fixedScriptWallet.supportsScriptType(this.name, addressType); } /** inherited doc */ @@ -774,7 +734,10 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici * @return true iff coin supports spending from chain */ supportsAddressChain(chain: number): boolean { - return isChainCode(chain) && this.supportsAddressType(utxolib.bitgo.scriptTypeForChain(chain)); + return ( + fixedScriptWallet.ChainCode.is(chain) && + this.supportsAddressType(fixedScriptWallet.ChainCode.scriptType(chain) as ScriptType2Of3) + ); } keyIdsForSigning(): number[] { @@ -1063,17 +1026,6 @@ export abstract class AbstractUtxoCoin extends BaseCoin implements Musig2Partici } const returnLegacyFormat = (params as Record).txFormat === 'legacy'; - - // In the case that we have a 'psbt-lite' transaction format, we want to indicate in signing to not fail - const txHex = (params.txHex ?? params.txPrebuild?.txHex) as string; - if ( - txHex && - utxolib.bitgo.isPsbt(txHex as string) && - utxolib.bitgo.isPsbtLite(utxolib.bitgo.createPsbtFromHex(txHex, this.network)) && - params.allowNonSegwitSigningWithoutPrevTx === undefined - ) { - return { ...params, allowNonSegwitSigningWithoutPrevTx: true, returnLegacyFormat }; - } return { ...params, returnLegacyFormat }; } diff --git a/modules/abstract-utxo/src/impl/doge/doge.ts b/modules/abstract-utxo/src/impl/doge/doge.ts index 79fe2da15c..f1ba54f030 100644 --- a/modules/abstract-utxo/src/impl/doge/doge.ts +++ b/modules/abstract-utxo/src/impl/doge/doge.ts @@ -1,5 +1,4 @@ import { BitGoBase, HalfSignedUtxoTransaction, SignedTransaction } from '@bitgo/sdk-core'; -import { bitgo } from '@bitgo/utxo-lib'; import { AbstractUtxoCoin, @@ -75,10 +74,6 @@ export class Doge extends AbstractUtxoCoin { /* postProcessPrebuild, isBitGoTaintedUnspent, verifyCustomChangeKeySignatures do not care whether they receive number or bigint */ - createTransactionFromHex(hex: string): bitgo.UtxoTransaction { - return super.createTransactionFromHex(hex); - } - async parseTransaction( params: ParseTransactionOptions ): /* diff --git a/modules/abstract-utxo/src/keychains.ts b/modules/abstract-utxo/src/keychains.ts index 073b40a848..2903a556bf 100644 --- a/modules/abstract-utxo/src/keychains.ts +++ b/modules/abstract-utxo/src/keychains.ts @@ -1,7 +1,6 @@ import assert from 'assert'; import * as t from 'io-ts'; -import { bitgo } from '@bitgo/utxo-lib'; import { IRequestTracer, IWallet, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core'; import { BIP32, bip32, fixedScriptWallet } from '@bitgo/wasm-utxo'; @@ -48,10 +47,10 @@ export function toKeychainTriple(keychains: UtxoNamedKeychains): Triple | string[] + keychains: fixedScriptWallet.RootWalletKeys | UtxoNamedKeychains | Triple<{ pub: string }> | string[] ): Triple { - if (keychains instanceof bitgo.RootWalletKeys) { - return keychains.triple.map((k) => BIP32.fromBase58(k.toBase58())) as Triple; + if (keychains instanceof fixedScriptWallet.RootWalletKeys) { + return [keychains.userKey(), keychains.backupKey(), keychains.bitgoKey()]; } if (Array.isArray(keychains)) { if (keychains.length !== 3) { diff --git a/modules/abstract-utxo/src/names.ts b/modules/abstract-utxo/src/names.ts index 555797ae3c..9e1a5c74c2 100644 --- a/modules/abstract-utxo/src/names.ts +++ b/modules/abstract-utxo/src/names.ts @@ -1,5 +1,3 @@ -import * as utxolib from '@bitgo/utxo-lib'; - export const utxoCoinsMainnet = ['btc', 'bch', 'bcha', 'bsv', 'btg', 'dash', 'doge', 'ltc', 'zec'] as const; export const utxoCoinsTestnet = [ 'tbtc', @@ -46,107 +44,6 @@ export function getMainnetCoinName(coinName: UtxoCoinName): UtxoCoinNameMainnet } } -function getNetworkName(n: utxolib.Network): utxolib.NetworkName { - const name = utxolib.getNetworkName(n); - if (!name) { - throw new Error('Unknown network'); - } - return name; -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param n - * @returns the family name for a network. Testnets and mainnets of the same coin share the same family name. - */ -export function getFamilyFromNetwork(n: utxolib.Network): UtxoCoinNameMainnet { - switch (getNetworkName(n)) { - case 'bitcoin': - case 'testnet': - case 'bitcoinPublicSignet': - case 'bitcoinTestnet4': - case 'bitcoinBitGoSignet': - return 'btc'; - case 'bitcoincash': - case 'bitcoincashTestnet': - return 'bch'; - case 'ecash': - case 'ecashTest': - return 'bcha'; - case 'bitcoingold': - case 'bitcoingoldTestnet': - return 'btg'; - case 'bitcoinsv': - case 'bitcoinsvTestnet': - return 'bsv'; - case 'dash': - case 'dashTest': - return 'dash'; - case 'dogecoin': - case 'dogecoinTest': - return 'doge'; - case 'litecoin': - case 'litecoinTest': - return 'ltc'; - case 'zcash': - case 'zcashTest': - return 'zec'; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * Get the chain name for a network. - * The chain is different for every network. - */ -export function getCoinName(n: utxolib.Network): UtxoCoinName { - switch (getNetworkName(n)) { - case 'bitcoinPublicSignet': - return 'tbtcsig'; - case 'bitcoinTestnet4': - return 'tbtc4'; - case 'bitcoinBitGoSignet': - return 'tbtcbgsig'; - case 'bitcoin': - case 'testnet': - case 'bitcoincash': - case 'bitcoincashTestnet': - case 'ecash': - case 'ecashTest': - case 'bitcoingold': - case 'bitcoingoldTestnet': - case 'bitcoinsv': - case 'bitcoinsvTestnet': - case 'dash': - case 'dashTest': - case 'dogecoin': - case 'dogecoinTest': - case 'litecoin': - case 'litecoinTest': - case 'zcash': - case 'zcashTest': - const mainnetName = getFamilyFromNetwork(n); - return utxolib.isTestnet(n) ? `t${mainnetName}` : mainnetName; - } -} - -/** - * @deprecated - will be removed when we drop support for utxolib - * @param coinName - the name of the coin (e.g. 'btc', 'bch', 'ltc'). Also called 'chain' in some contexts. - * @returns the network for a coin. This is the mainnet network for the coin. - */ -export function getNetworkFromCoinName(coinName: string): utxolib.Network { - for (const network of utxolib.getNetworkList()) { - if (getCoinName(network) === coinName) { - return network; - } - } - throw new Error(`Unknown coin name ${coinName}`); -} - -/** @deprecated - use getNetworkFromCoinName instead */ -export const getNetworkFromChain = getNetworkFromCoinName; - function getBaseNameFromMainnet(coinName: UtxoCoinNameMainnet): string { switch (coinName) { case 'btc': @@ -189,11 +86,6 @@ export function getFullNameFromCoinName(coinName: UtxoCoinName): string { return prefix + getBaseNameFromMainnet(getMainnetCoinName(coinName)); } -/** @deprecated - use getFullNameFromCoinName instead */ -export function getFullNameFromNetwork(n: utxolib.Network): string { - return getFullNameFromCoinName(getCoinName(n)); -} - export function isTestnetCoin(coinName: UtxoCoinName): boolean { return isUtxoCoinNameTestnet(coinName); } diff --git a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts index 3cf4c50052..d041049896 100644 --- a/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/src/recovery/backupKeyRecovery.ts @@ -8,7 +8,7 @@ import { krsProviders, Triple, } from '@bitgo/sdk-core'; -import { BIP32, fixedScriptWallet } from '@bitgo/wasm-utxo'; +import { BIP32, fixedScriptWallet, Transaction } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin } from '../abstractUtxoCoin'; import { signAndVerifyPsbt } from '../transaction/fixedScript/signTransaction'; @@ -172,8 +172,8 @@ async function queryBlockchainUnspentsPath( // json parse won't parse it correctly, so we requery the txid for the tx hex to decode here if (!Number.isSafeInteger(u.value)) { const txHex = await getPrevTx(txid); - const tx = coin.createTransactionFromHex(txHex); - val = tx.outs[vout].value; + const tx = Transaction.fromBytes(Buffer.from(txHex, 'hex')); + val = tx.getOutputs()[vout].value; } } // the api may return cashaddr's instead of legacy for BCH and BCHA diff --git a/modules/abstract-utxo/src/transaction/decode.ts b/modules/abstract-utxo/src/transaction/decode.ts index 556e5e73d4..35f4337582 100644 --- a/modules/abstract-utxo/src/transaction/decode.ts +++ b/modules/abstract-utxo/src/transaction/decode.ts @@ -1,7 +1,6 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt, utxolibCompat } from '@bitgo/wasm-utxo'; +import { fixedScriptWallet, hasPsbtMagic, Psbt as WasmPsbt } from '@bitgo/wasm-utxo'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; +import { UtxoCoinName } from '../names'; import { BitGoPsbt } from './types'; @@ -22,20 +21,11 @@ export function stringToBufferTryFormats(input: string, formats: BufferEncoding[ throw new Error('input must be a valid hex or base64 string'); } -function toNetworkName(coinName: UtxoCoinName): utxolibCompat.UtxolibName { - const network = getNetworkFromCoinName(coinName); - const networkName = utxolib.getNetworkName(network); - if (!networkName) { - throw new Error(`Invalid coinName: ${coinName}`); - } - return networkName; -} - export function decodePsbt(psbt: string | Buffer, coinName: UtxoCoinName): BitGoPsbt { if (typeof psbt === 'string') { psbt = Buffer.from(psbt, 'hex'); } - return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(coinName)); + return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, coinName); } export type PrebuildLike = { diff --git a/modules/abstract-utxo/src/transaction/fetchInputs.ts b/modules/abstract-utxo/src/transaction/fetchInputs.ts deleted file mode 100644 index 01459430a4..0000000000 --- a/modules/abstract-utxo/src/transaction/fetchInputs.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as utxolib from '@bitgo/utxo-lib'; -import { BitGoBase, IRequestTracer } from '@bitgo/sdk-core'; - -import { AbstractUtxoCoin, TransactionPrebuild } from '../abstractUtxoCoin'; -import { getNetworkFromCoinName, UtxoCoinName } from '../names'; - -/** - * Get the inputs for a psbt from a prebuild. - */ -export function getPsbtTxInputs( - psbtArg: string | utxolib.bitgo.UtxoPsbt, - coinName: UtxoCoinName -): { address: string; value: bigint; valueString: string }[] { - const network = getNetworkFromCoinName(coinName); - const psbt = psbtArg instanceof utxolib.bitgo.UtxoPsbt ? psbtArg : utxolib.bitgo.createPsbtFromHex(psbtArg, network); - const txInputs = psbt.txInputs; - return psbt.data.inputs.map((input, index) => { - let address: string; - let value: bigint; - if (input.witnessUtxo) { - address = utxolib.address.fromOutputScript(input.witnessUtxo.script, network); - value = input.witnessUtxo.value; - } else if (input.nonWitnessUtxo) { - const tx = utxolib.bitgo.createTransactionFromBuffer(input.nonWitnessUtxo, network, { - amountType: 'bigint', - }); - const txId = (Buffer.from(txInputs[index].hash).reverse() as Buffer).toString('hex'); - if (tx.getId() !== txId) { - throw new Error('input transaction hex does not match id'); - } - const prevTxOutputIndex = txInputs[index].index; - address = utxolib.address.fromOutputScript(tx.outs[prevTxOutputIndex].script, network); - value = tx.outs[prevTxOutputIndex].value; - } else { - throw new Error('psbt input is missing both witnessUtxo and nonWitnessUtxo'); - } - return { address, value, valueString: value.toString() }; - }); -} - -/** - * Get the inputs for a transaction from a prebuild. - */ -export async function getTxInputs(params: { - txPrebuild: TransactionPrebuild; - bitgo: BitGoBase; - coin: AbstractUtxoCoin; - disableNetworking: boolean; - reqId?: IRequestTracer; -}): Promise<{ address: string; value: TNumber; valueString: string }[]> { - const { txPrebuild, bitgo, coin, disableNetworking, reqId } = params; - if (!txPrebuild.txHex) { - throw new Error(`txPrebuild.txHex not set`); - } - const transaction = coin.createTransactionFromHex(txPrebuild.txHex); - const transactionCache = {}; - return await Promise.all( - transaction.ins.map(async (currentInput): Promise<{ address: string; value: TNumber; valueString: string }> => { - const transactionId = (Buffer.from(currentInput.hash).reverse() as Buffer).toString('hex'); - const txHex = txPrebuild.txInfo?.txHexes?.[transactionId]; - if (txHex) { - const localTx = coin.createTransactionFromHex(txHex); - if (localTx.getId() !== transactionId) { - throw new Error('input transaction hex does not match id'); - } - const currentOutput = localTx.outs[currentInput.index]; - const address = utxolib.address.fromOutputScript(currentOutput.script, coin.network); - return { - address, - value: currentOutput.value, - valueString: currentOutput.value.toString(), - }; - } else if (!transactionCache[transactionId]) { - if (disableNetworking) { - throw new Error('attempting to retrieve transaction details externally with networking disabled'); - } - if (reqId) { - bitgo.setRequestTracer(reqId); - } - transactionCache[transactionId] = await bitgo.get(coin.url(`/public/tx/${transactionId}`)).result(); - } - const transactionDetails = transactionCache[transactionId]; - return transactionDetails.outputs[currentInput.index]; - }) - ); -} diff --git a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts index 49c333c4f0..a9dec08532 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/signTransaction.ts @@ -33,8 +33,6 @@ export async function signTransaction( txInfo: { unspents?: Unspent[] } | undefined; isLastSignature: boolean; signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined; - /** deprecated */ - allowNonSegwitSigningWithoutPrevTx: boolean; pubs: string[] | undefined; cosignerPub: string | undefined; /** When true (default), extract finalized PSBT to legacy transaction format. When false, return finalized PSBT. */ diff --git a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts index 9f4aba34a9..eff4f13df3 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/verifyTransaction.ts @@ -5,11 +5,9 @@ import { BitGoBase, TxIntentMismatchError, IBaseCoin } from '@bitgo/sdk-core'; import { hasPsbtMagic } from '@bitgo/wasm-utxo'; import { AbstractUtxoCoin, VerifyTransactionOptions } from '../../abstractUtxoCoin'; -import { Output, ParsedTransaction } from '../types'; -import { toTNumber } from '../../tnumber'; +import { ParsedTransaction } from '../types'; import { stringToBufferTryFormats } from '../decode'; import { verifyCustomChangeKeySignatures, verifyKeySignature, verifyUserPublicKeyAsync } from '../../verifyKey'; -import { getPsbtTxInputs, getTxInputs } from '../fetchInputs'; const debug = buildDebug('bitgo:abstract-utxo:verifyTransaction'); @@ -201,29 +199,9 @@ export async function verifyTransaction( } } - const allOutputs = parsedTransaction.outputs; if (!txPrebuild.txHex) { throw new Error(`txPrebuild.txHex not set`); } - const inputs = isPsbt - ? getPsbtTxInputs(txPrebuild.txHex, coin.name).map((v) => ({ - ...v, - value: toTNumber(v.value, coin.amountType), - })) - : await getTxInputs({ txPrebuild, bitgo, coin, disableNetworking, reqId }); - // coins (doge) that can exceed number limits (and thus will use bigint) will have the `valueString` field - const inputAmount = inputs.reduce( - (sum: bigint, i) => sum + BigInt(coin.amountType === 'bigint' ? i.valueString : i.value), - BigInt(0) - ); - const outputAmount = allOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)); - const fee = inputAmount - outputAmount; - - if (fee < 0) { - throw new Error( - `attempting to spend ${outputAmount} satoshis, which exceeds the input amount (${inputAmount} satoshis) by ${-fee}` - ); - } return true; } diff --git a/modules/abstract-utxo/src/transaction/index.ts b/modules/abstract-utxo/src/transaction/index.ts index 075ef05742..d0231ff6cb 100644 --- a/modules/abstract-utxo/src/transaction/index.ts +++ b/modules/abstract-utxo/src/transaction/index.ts @@ -3,7 +3,6 @@ export * from './recipient'; export { explainTx } from './explainTransaction'; export { parseTransaction } from './parseTransaction'; export { verifyTransaction } from './verifyTransaction'; -export * from './fetchInputs'; export * as bip322 from './bip322'; export { decodePsbt } from './decode'; export * from './fixedScript'; diff --git a/modules/abstract-utxo/src/transaction/signTransaction.ts b/modules/abstract-utxo/src/transaction/signTransaction.ts index 23f27de260..7a08d0b79f 100644 --- a/modules/abstract-utxo/src/transaction/signTransaction.ts +++ b/modules/abstract-utxo/src/transaction/signTransaction.ts @@ -65,7 +65,6 @@ export async function signTransaction( txInfo: params.txPrebuild.txInfo, isLastSignature: params.isLastSignature ?? false, signingStep: params.signingStep, - allowNonSegwitSigningWithoutPrevTx: params.allowNonSegwitSigningWithoutPrevTx ?? false, pubs: params.pubs, cosignerPub: params.cosignerPub, extractTransaction: params.extractTransaction, diff --git a/modules/abstract-utxo/src/wasmUtil.ts b/modules/abstract-utxo/src/wasmUtil.ts deleted file mode 100644 index f22e4fcf50..0000000000 --- a/modules/abstract-utxo/src/wasmUtil.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { BIP32, bip32, ECPair, Psbt, descriptorWallet } from '@bitgo/wasm-utxo'; -import * as utxolib from '@bitgo/utxo-lib'; - -export type BIP32Key = BIP32 | bip32.BIP32Interface | utxolib.BIP32Interface; -export type ECPairKey = ECPair | utxolib.ECPairInterface | Uint8Array; -export type UtxoLibPsbt = utxolib.Psbt | utxolib.bitgo.UtxoPsbt; - -/** - * Map of descriptor name to Descriptor instance. - * Re-exported from wasm-utxo for consistency. - */ -export type DescriptorMap = descriptorWallet.DescriptorMap; - -/** - * Key type accepted by descriptorWallet.signWithKey - */ -export type SignerKey = Parameters[1]; - -/** - * Convert a utxo-lib BIP32Interface to a wasm-utxo BIP32 instance. - * Preserves private key by using base58 serialization. - */ -export function toWasmBIP32(key: BIP32Key): BIP32 { - if (key instanceof BIP32) { - return key; - } - // All utxo-lib BIP32Interface instances have toBase58 - return BIP32.fromBase58(key.toBase58()); -} - -/** - * Convert a wasm-utxo BIP32 to a utxo-lib BIP32Interface. - * Used at boundaries where utxo-lib APIs require their own BIP32Interface type. - */ -export function toUtxolibBIP32(key: BIP32Key): utxolib.BIP32Interface { - return utxolib.bip32.fromBase58(key.toBase58()); -} - -export function toWasmECPair(key: ECPairKey): ECPair { - if (key instanceof ECPair) { - return key; - } - if (key instanceof Uint8Array) { - return ECPair.from(key); - } - if (key.privateKey) { - return ECPair.fromPrivateKey(key.privateKey); - } - return ECPair.fromPublicKey(key.publicKey); -} - -export function isUtxoLibPsbt(psbt: unknown): psbt is UtxoLibPsbt { - return psbt instanceof utxolib.Psbt || psbt instanceof utxolib.bitgo.UtxoPsbt; -} - -export function toWasmPsbt(psbt: Psbt | UtxoLibPsbt | Uint8Array): Psbt { - if (psbt instanceof Psbt) { - return psbt; - } - if (psbt instanceof Uint8Array) { - return Psbt.deserialize(psbt); - } - if (isUtxoLibPsbt(psbt)) { - return Psbt.deserialize(psbt.toBuffer()); - } - throw new Error('Unsupported PSBT type'); -} - -/** - * Sum the `value` property of an array of objects. - */ -export function sumValues(arr: { value: bigint }[]): bigint { - return arr.reduce((sum, e) => sum + e.value, BigInt(0)); -} diff --git a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts index 2beb751815..a6b064904b 100644 --- a/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts +++ b/modules/abstract-utxo/test/unit/buildSignSendLegacyFormat.ts @@ -11,6 +11,7 @@ import { encryptKeychain, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -46,12 +47,17 @@ describe('prebuildAndSign-returnLegacyFormat', function () { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, amount: outputAmount.toString() }; prebuild = utxolib.testutil.constructPsbt( inputScripts.map((s) => ({ scriptType: s, value: BigInt(1e8) })), [{ scriptType: outputScriptType, value: outputAmount }], - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/customSigner.ts b/modules/abstract-utxo/test/unit/customSigner.ts index 0e673009aa..681c4155ff 100644 --- a/modules/abstract-utxo/test/unit/customSigner.ts +++ b/modules/abstract-utxo/test/unit/customSigner.ts @@ -3,7 +3,14 @@ import nock = require('nock'); import * as sinon from 'sinon'; import { CustomSigningFunction, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, getUtxoCoin, getUtxoWallet, assertHasProperty } from './util'; +import { + defaultBitGo, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + getUtxoWallet, + assertHasProperty, +} from './util'; nock.disableNetConnect(); @@ -56,7 +63,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); @@ -72,7 +79,7 @@ describe('UTXO Custom Signer Function', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2sh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); diff --git a/modules/abstract-utxo/test/unit/prebuildAndSign.ts b/modules/abstract-utxo/test/unit/prebuildAndSign.ts index e2ae0dd0ec..24910bea5b 100644 --- a/modules/abstract-utxo/test/unit/prebuildAndSign.ts +++ b/modules/abstract-utxo/test/unit/prebuildAndSign.ts @@ -12,6 +12,7 @@ import { defaultBitGo, getDefaultWalletKeys, getMinUtxoCoins, + getNetworkForCoinName, getUtxoWallet, keychainsBase58, getScriptTypes, @@ -62,7 +63,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const psbt = utxolib.testutil.constructPsbt( inputs as utxolib.testutil.Input[], outputs, - coin.network, + getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { @@ -178,7 +179,12 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee; const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh'; const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType); - const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network); + const outputAddress = utxolib.bitgo.getWalletAddress( + rootWalletKeys, + outputChain, + 0, + getNetworkForCoinName(coin.name) + ); recipient = { address: outputAddress, @@ -222,7 +228,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); [true, false].forEach((selfSend) => { @@ -254,7 +260,7 @@ function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[]): void { nocks.forEach((nock) => assert.ok(nock.isDone())); - assertSignable(res.txHex, inputScripts, coin.network); + assertSignable(res.txHex, inputScripts, getNetworkForCoinName(coin.name)); }); }); }); diff --git a/modules/abstract-utxo/test/unit/signTransaction.ts b/modules/abstract-utxo/test/unit/signTransaction.ts index 7491ecf82b..f5dfd0e111 100644 --- a/modules/abstract-utxo/test/unit/signTransaction.ts +++ b/modules/abstract-utxo/test/unit/signTransaction.ts @@ -9,7 +9,14 @@ import { common, Triple } from '@bitgo/sdk-core'; import { getReplayProtectionPubkeys, ErrorDeprecatedTxFormat } from '../../src'; import type { Unspent } from '../../src/unspent'; -import { getUtxoWallet, getDefaultWalletKeys, getUtxoCoin, keychainsBase58, defaultBitGo } from './util'; +import { + getUtxoWallet, + getDefaultWalletKeys, + getNetworkForCoinName, + getUtxoCoin, + keychainsBase58, + defaultBitGo, +} from './util'; describe('signTransaction', function () { const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; @@ -21,7 +28,7 @@ describe('signTransaction', function () { const pubs = keychainsBase58.map((v) => v.pub) as Triple; function validatePsbt(txHex: string, targetSigCount: 0 | 1, targetNonceCount?: 1 | 2) { - const psbt = utxolib.bitgo.createPsbtFromHex(txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(txHex, getNetworkForCoinName(coin.name)); psbt.data.inputs.forEach((input, index) => { const parsed = utxolib.bitgo.parsePsbtInput(input); if (parsed.scriptType === 'taprootKeyPathSpend') { @@ -38,7 +45,7 @@ describe('signTransaction', function () { } function validateTx(txHex: string, unspents: Unspent[], targetSigCount: 0 | 1) { - const tx = utxolib.bitgo.createTransactionFromHex(txHex, coin.network); + const tx = utxolib.bitgo.createTransactionFromHex(txHex, getNetworkForCoinName(coin.name)); unspents.forEach((u, i) => { const sigCount = utxolib.bitgo.getStrictSignatureCount(tx.ins[i]); const expectedSigCount = utxolib.bitgo.isWalletUnspent(u) && !!targetSigCount ? 1 : 0; @@ -56,7 +63,7 @@ describe('signTransaction', function () { const txHex = tx.toHex(); function nockSignPsbt(psbtHex: string): nock.Scope { - const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, getNetworkForCoinName(coin.name)); return nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, { psbt: psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo).toHex() }); @@ -147,7 +154,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -163,7 +170,7 @@ describe('signTransaction', function () { .map((scriptType) => ({ scriptType, value: BigInt(1000) })); const unspentSum = inputs.reduce((prev: bigint, cur) => prev + cur.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned', { p2shP2pkKey: replayProtectionKey, }); @@ -181,8 +188,16 @@ describe('signTransaction', function () { })); const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.TxnOutput[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const txBuilder = testutil.constructTxnBuilder(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); - const unspents = inputs.map((v, i) => testutil.toTxnUnspent(v, i, coin.network, rootWalletKeys)); + const txBuilder = testutil.constructTxnBuilder( + inputs, + outputs, + getNetworkForCoinName(coin.name), + rootWalletKeys, + 'unsigned' + ); + const unspents = inputs.map((v, i) => + testutil.toTxnUnspent(v, i, getNetworkForCoinName(coin.name), rootWalletKeys) + ); // Legacy format transactions are now deprecated and should throw ErrorDeprecatedTxFormat await assert.rejects(async () => { @@ -194,7 +209,7 @@ describe('signTransaction', function () { const inputs: testutil.Input[] = [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }]; const unspentSum = inputs.reduce((prev: bigint, curr) => prev + curr.value, BigInt(0)); const outputs: testutil.Output[] = [{ scriptType: 'p2sh', value: unspentSum - BigInt(1000) }]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, rootWalletKeys, 'unsigned'); + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), rootWalletKeys, 'unsigned'); await assert.rejects( async () => { diff --git a/modules/abstract-utxo/test/unit/transaction.ts b/modules/abstract-utxo/test/unit/transaction.ts index ed69fa610e..09f8d51201 100644 --- a/modules/abstract-utxo/test/unit/transaction.ts +++ b/modules/abstract-utxo/test/unit/transaction.ts @@ -31,6 +31,7 @@ import { getWalletKeys, defaultBitGo, getMinUtxoCoins, + getNetworkForCoinName, getScriptTypes, } from './util'; @@ -60,7 +61,7 @@ function run( return testutil.toUnspent( { scriptType: t, value: t === 'p2shP2pk' ? BigInt(1000) : BigInt(value) }, index, - coin.network, + getNetworkForCoinName(coin.name), walletKeys ); }); @@ -72,7 +73,7 @@ function run( function getUnspents(): Unspent[] { return inputScripts.map((type, i) => - mockUnspent(coin.network, walletKeys, toTxnInputScriptType(type), i, value) + mockUnspent(getNetworkForCoinName(coin.name), walletKeys, toTxnInputScriptType(type), i, value) ); } @@ -113,7 +114,9 @@ function run( scope = nock(bgUrl) .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/signpsbt`, (body) => body.psbt) .reply(200, (_uri: string, requestBody: unknown) => { - const networkName = utxolib.getNetworkName(coin.network) as fixedScriptWallet.NetworkName; + const networkName = utxolib.getNetworkName( + getNetworkForCoinName(coin.name) + ) as fixedScriptWallet.NetworkName; const reqBytes = Buffer.from((requestBody as { psbt: string }).psbt, 'hex'); const reqPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(reqBytes, networkName); const cosignerWasm = BIP32.fromBase58(cosigner.toBase58()); @@ -166,7 +169,7 @@ function run( const outputs: testutil.Output[] = [ { address: getOutputAddress(getWalletKeys('test')), value: unspentSum - BigInt(1000) }, ]; - const psbt = testutil.constructPsbt(inputs, outputs, coin.network, walletKeys, 'unsigned', { + const psbt = testutil.constructPsbt(inputs, outputs, getNetworkForCoinName(coin.name), walletKeys, 'unsigned', { p2shP2pkKey: getReplayProtectionPubkeys(coin.name)[0], }); utxolib.bitgo.addXpubsToPsbt(psbt, walletKeys); @@ -177,7 +180,11 @@ function run( const prebuild = txFormat === 'psbt' ? createPrebuildPsbt() - : createPrebuildTransaction(coin.network, getUnspents(), getOutputAddress(walletKeys)); + : createPrebuildTransaction( + getNetworkForCoinName(coin.name), + getUnspents(), + getOutputAddress(walletKeys) + ); const halfSignedUserBitGo = await createHalfSignedTransaction(prebuild, walletKeys.user, walletKeys.bitgo); const fullSignedUserBitGo = @@ -225,7 +232,7 @@ function run( ? undefined : v instanceof utxolib.bitgo.UtxoTransaction ? transactionToObj(v) - : transactionHexToObj(v.txHex, coin.network, amountType) + : transactionHexToObj(v.txHex, getNetworkForCoinName(coin.name), amountType) ) as TransactionObjStages; } @@ -240,7 +247,7 @@ function run( }); function testPsbtValidSignatures(tx: HalfSignedUtxoTransaction, signedBy: BIP32Interface[]) { - const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, coin.network); + const psbt = utxolib.bitgo.createPsbtFromHex(tx.txHex, getNetworkForCoinName(coin.name)); const unspents = getUnspentsForPsbt(); psbt.data.inputs.forEach((input, index) => { const unspent = unspents[index]; @@ -278,7 +285,7 @@ function run( const transaction = utxolib.bitgo.createTransactionFromBuffer( Buffer.from(tx.txHex, 'hex'), - coin.network, + getNetworkForCoinName(coin.name), { amountType } ); transaction.ins.forEach((input, index) => { diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts index 1d7e29d091..2cc561e871 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts @@ -94,6 +94,33 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { describe('explainPsbt(Wasm)', function () { testutil.AcidTest.suite().forEach((test) => describeTransactionWith(test)); + + it('explainPsbtWasmBigInt throws when total output value exceeds total input value', function () { + const network = utxolib.networks.bitcoin; + const rootWalletKeys = testutil.getDefaultWalletKeys(); + // Valid PSBT: one p2wsh input (5000 sat), one internal p2wsh output (3000 sat), fee = 2000 + const psbt = testutil.constructPsbt( + [{ scriptType: 'p2wsh', value: 5000n }], + [{ scriptType: 'p2wsh', value: 3000n, isInternalAddress: true }], + network, + rootWalletKeys, + 'unsigned' + ); + // Tamper: reduce the witnessUtxo.value so that inputs (1000) < outputs (3000). + // Direct mutation avoids bip174's "duplicate data" guard on updateInput. + psbt.data.inputs[0].witnessUtxo!.value = 1000n; + + const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'bitcoin'); + const walletXpubs = fixedScriptWallet.RootWalletKeys.from(rootWalletKeys); + + assert.throws( + () => + explainPsbtWasmBigInt(wasmPsbt, walletXpubs, { + replayProtection: { publicKeys: [] }, + }), + /Fee calculation error: outputs exceed inputs/ + ); + }); }); describe('aggregateTransactionExplanations', function () { diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index d129f40614..e6c2676073 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -8,10 +8,9 @@ import { fixedScriptWallet } from '@bitgo/wasm-utxo'; import { parseTransaction } from '../../../../src/transaction/fixedScript/parseTransaction'; import { ParsedTransaction } from '../../../../src/transaction/types'; import { UtxoWallet } from '../../../../src/wallet'; -import { getUtxoCoin } from '../../util'; +import { getCoinNameForNetwork, getUtxoCoin } from '../../util'; import { explainPsbtWasm } from '../../../../src/transaction/fixedScript'; import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction'; -import { getCoinName } from '../../../../src/names'; import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin'; function getTxParamsFromExplanation( @@ -78,7 +77,7 @@ function describeParseTransactionWith( let stubExplainTransaction: sinon.SinonStub; before('prepare', async function () { - const coinName = getCoinName(acidTest.network); + const coinName = getCoinNameForNetwork(acidTest.network); coin = getUtxoCoin(coinName); // Create PSBT and explanation diff --git a/modules/abstract-utxo/test/unit/verifyTransaction.ts b/modules/abstract-utxo/test/unit/verifyTransaction.ts index 35b58a9568..70c0ef9946 100644 --- a/modules/abstract-utxo/test/unit/verifyTransaction.ts +++ b/modules/abstract-utxo/test/unit/verifyTransaction.ts @@ -1,6 +1,5 @@ import assert from 'assert'; -import * as utxolib from '@bitgo/utxo-lib'; import * as sinon from 'sinon'; import { Wallet } from '@bitgo/sdk-core'; @@ -217,10 +216,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -234,7 +229,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should not allow any implicit external outputs if paygo outputs are disallowed', async () => { @@ -284,10 +278,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -302,7 +292,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should verify a bridging transaction whose implicit external output matches the bridge amount', async () => { @@ -329,10 +318,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -349,7 +334,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); it('should reject a bridging transaction whose implicit external output does not match the bridge amount', async () => { @@ -560,10 +544,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(coin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await coin.verifyTransaction({ txParams: { walletPassphrase: passphrase }, txPrebuild: { txHex: '00' }, @@ -574,7 +554,6 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); }); @@ -605,10 +584,6 @@ describe('Verify Transaction', function () { needsCustomChangeKeySignatureVerification: false, }); - const bitcoinMock = sinon - .stub(bigintCoin, 'createTransactionFromHex') - .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction); - const result = await bigintCoin.verifyTransaction({ txParams: { walletPassphrase: passphrase, @@ -623,6 +598,5 @@ describe('Verify Transaction', function () { assert.strictEqual(result, true); coinMock.restore(); - bitcoinMock.restore(); }); }); diff --git a/modules/abstract-utxo/test/unit/wallet.ts b/modules/abstract-utxo/test/unit/wallet.ts index 7c9b6223e7..4313a70237 100644 --- a/modules/abstract-utxo/test/unit/wallet.ts +++ b/modules/abstract-utxo/test/unit/wallet.ts @@ -5,7 +5,7 @@ import nock = require('nock'); import * as _ from 'lodash'; import { Wallet, ManageUnspentsOptions, common } from '@bitgo/sdk-core'; -import { defaultBitGo, getDefaultWalletKeys, toKeychainObjects, getUtxoCoin } from './util'; +import { defaultBitGo, getDefaultWalletKeys, getNetworkForCoinName, toKeychainObjects, getUtxoCoin } from './util'; const bgUrl = common.Environments[defaultBitGo.getEnv()].uri; const bitgo = defaultBitGo; @@ -35,7 +35,7 @@ describe('manage unspents', function () { utxoLib.testutil.constructPsbt( [{ scriptType, value: BigInt(1000) }], [{ scriptType, value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ) @@ -74,7 +74,7 @@ describe('manage unspents', function () { const psbt = utxoLib.testutil.constructPsbt( [{ scriptType: 'p2wsh', value: BigInt(1000) }], [{ scriptType: 'p2shP2wsh', value: BigInt(900) }], - basecoin.network, + getNetworkForCoinName(basecoin.name), rootWalletKey, 'unsigned' ); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts index 6e9c010a43..cb3cc68216 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts @@ -14,6 +14,7 @@ import { import { BitGo, createSharedDataProof, TssUtils, RequestType } from '../../../../../src'; import { BackupGpgKey, + AddKeychainOptions, BackupKeyShare, BaseCoin, BitgoGPGPublicKey, @@ -309,6 +310,48 @@ describe('TSS Ecdsa Utils:', async function () { should.exist(backupKeychain.encryptedPrv); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + // Keep the real crypto deps (constants w/ bitgo gpg key for verifyWalletSignatures) and + // capture the user keychain add() params by stubbing baseCoin.keychains(). + nock.cleanAll(); + nock(bgUrl) + .get('/api/v1/client/constants') + .times(16) + .reply(200, { ttl: 3600, constants: { mpc: { bitgoPublicKey: bitGoGPGKeyPair.publicKey } } }); + + const addStub = sandbox.stub().resolves({ id: '1', pub: '', type: 'tss' }); + sandbox.stub(baseCoin, 'keychains').returns({ add: addStub } as unknown as ReturnType); + + const enterpriseId = 'enterprise_id'; + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + await tssUtils.createParticipantKeychain( + userGpgKey, + userLocalBackupGpgKey, + bitgoPublicKey, + 1, + userKeyShare, + backupKeyShare, + nockedBitGoKeychain, + 'passphrase', + undefined, + webauthnInfo, + undefined, + enterpriseId + ); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + assert.ok(addStub.calledOnce, 'keychains().add should have been called for the user keychain'); + const body = addStub.firstCall.args[0] as AddKeychainOptions; + assert.ok(body.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(body.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(body.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(body.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(body.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: body.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(body.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should generate TSS key chains with optional params', async function () { const enterprise = 'enterprise_id'; const backupShareHolder: BackupKeyShare = { diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts index dee40f849f..94e6d08830 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts @@ -57,6 +57,9 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { }); before(async function () { + // Allow secp256k1 GPG keys used by these fixtures (the full suite enables this + // globally via sibling test files; set it here so this file also runs in isolation). + openpgp.config.rejectCurves = new Set(); bitGoGgpKey = await openpgp.generateKey({ userIDs: [ { @@ -176,6 +179,68 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { assert.equal(bitgoKeychain.source, 'bitgo'); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const bitgoSession = new DklsDkg.Dkg(3, 2, 2); + + const round1Nock = await nockKeyGenRound1(bitgoSession, 1); + const round2Nock = await nockKeyGenRound2(bitgoSession, 1); + const round3Nock = await nockKeyGenRound3(bitgoSession, 1); + + // Capture each keychain POST body by source so we can assert what the user key sends. + const capturedBodies: Record = {}; + const addKeyNock = nock('https://bitgo.fakeurl') + .post(`/api/v2/${coinName}/key`, (body) => body.keyType === 'tss' && body.isMPCv2) + .times(3) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + capturedBodies[requestBody.source as string] = requestBody; + const key = { + id: requestBody.source, + source: requestBody.source, + type: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + nock('https://bitgo.fakeurl').get(`/api/v2/${coinName}/key/${requestBody.source}`).reply(200, key); + return key; + }); + + const webauthnInfo = { + otpDeviceId: 'device-123', + prfSalt: 'salt-abc', + passphrase: 'prf-derived-passphrase', + }; + const params = { + passphrase: 'test', + enterprise: enterpriseId, + originalPasscodeEncryptionCode: '123456', + webauthnInfo, + }; + await tssUtils.createKeychains(params); + assert.ok(round1Nock.isDone()); + assert.ok(round2Nock.isDone()); + assert.ok(round3Nock.isDone()); + assert.ok(addKeyNock.isDone()); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), + // including enterpriseId, and must NOT use the deprecated webauthnDevices array. + const userBody = capturedBodies['user']; + assert.ok(userBody, 'user keychain should have been created'); + assert.ok(userBody.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(userBody.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(userBody.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(userBody.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(userBody.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + // encryptedPrv is the user key share encrypted with the PRF-derived passphrase. + assert.ok(bitgo.decrypt({ input: userBody.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(userBody.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + + // Backup keychain must never carry passkey material. + const backupBody = capturedBodies['backup']; + assert.ok(backupBody, 'backup keychain should have been created'); + assert.strictEqual(backupBody.webauthnInfo, undefined); + assert.strictEqual(backupBody.webauthnDevices, undefined); + }); + it('should generate TSS MPCv2 keys with v2 encryption envelopes', async function () { const bitgoSession = new DklsDkg.Dkg(3, 2, 2); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index d3dea3383b..f1daae1f91 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -1,3 +1,4 @@ +import * as assert from 'assert'; import * as sodium from 'libsodium-wrappers-sumo'; import * as _ from 'lodash'; import nock = require('nock'); @@ -8,6 +9,7 @@ import * as sinon from 'sinon'; import { TestableBG, TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../../../src'; import { + AddKeychainOptions, BaseCoin, BitgoGPGPublicKey, CommitmentShareRecord, @@ -269,6 +271,62 @@ describe('TSS Utils:', async function () { should.exist(backupKeychain.encryptedPrv); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const userKeyShare = MPC.keyShare(1, 2, 3); + const backupKeyShare = MPC.keyShare(2, 2, 3); + + // Real crypto deps (constants w/ bitgo gpg key for verifyWalletSignatures + bitgo keychain), + // then capture the user keychain add() params by stubbing baseCoin.keychains(). + nock.cleanAll(); + nock(bgUrl) + .get('/api/v1/client/constants') + .times(23) + .reply(200, { ttl: 3600, constants: { mpc: { bitgoPublicKey: bitgoGpgKey.publicKey } } }); + await nockBitgoKeychain({ + coin: coinName, + userKeyShare, + backupKeyShare, + bitgoKeyShare, + userGpgKey, + backupGpgKey, + bitgoGpgKey, + }); + const bitgoKeychain = await tssUtils.createBitgoKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + }); + + const addStub = sandbox.stub().resolves({ id: '1', pub: '', type: 'tss' }); + sandbox.stub(baseCoin, 'keychains').returns({ add: addStub } as unknown as ReturnType); + + const enterpriseId = 'enterprise_id'; + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + await tssUtils.createUserKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + bitgoKeychain, + passphrase: 'passphrase', + webauthnInfo, + enterprise: enterpriseId, + }); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + assert.ok(addStub.calledOnce, 'keychains().add should have been called for the user keychain'); + const body = addStub.firstCall.args[0] as AddKeychainOptions; + assert.ok(body.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(body.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(body.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(body.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(body.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: body.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(body.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should generate TSS key chains without passphrase', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts index 6cb2f33216..53ae6fe795 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts @@ -109,6 +109,52 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { assert.equal(userKeychain.commonKeychain, bitgoKeychain.commonKeychain); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const commonKeychain = 'a'.repeat(64); + const capturedBodies: Record = {}; + const addKeyNock = nock('https://bitgo.fakeurl') + .post(`/api/v2/${coinName}/key`, (body) => body.keyType === 'tss' && body.isMPCv2) + .times(1) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + capturedBodies[requestBody.source as string] = requestBody; + return { + id: requestBody.source, + source: requestBody.source, + type: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + }); + + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + // Direct participant-keychain call avoids the EdDSA DKG ceremony while still exercising the + // user-keychain webauthn assembly that POSTs to /key. + await tssUtils.createParticipantKeychain( + MPCv2PartiesEnum.USER, + commonKeychain, + Buffer.from('userPrivate'), + Buffer.from('userReduced'), + 'passphrase', + undefined, + webauthnInfo, + undefined, + enterpriseId + ); + assert.ok(addKeyNock.isDone()); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + const userBody = capturedBodies['user']; + assert.ok(userBody, 'user keychain should have been created'); + assert.ok(userBody.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(userBody.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(userBody.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(userBody.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(userBody.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: userBody.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(userBody.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should create TSS key chains', async function () { const fakeCommonKeychain = 'a'.repeat(64); diff --git a/modules/sdk-coin-canton/src/lib/utils.ts b/modules/sdk-coin-canton/src/lib/utils.ts index c5d94cf8bb..13cecca6a3 100644 --- a/modules/sdk-coin-canton/src/lib/utils.ts +++ b/modules/sdk-coin-canton/src/lib/utils.ts @@ -352,13 +352,13 @@ export class Utils implements BaseUtils { } case TransactionType.CosignDelegationAccept: { - // exercise CosignDelegationProposal_Accept → actingParties[0] = signer (sender) - const signerParty = findExerciseActingParty('CosignDelegationProposal_Accept'); + // exercise CosigningDelegationProposal_Accept → actingParties[0] = signer (sender) + const signerParty = findExerciseActingParty('CosigningDelegationProposal_Accept'); if (signerParty) sender = signerParty; - // CosignDelegationProposal create node → admin = receiver - const proposalFields = findCreateNodeFields('CosignDelegationProposal'); - if (proposalFields) { - const adminData = getField(proposalFields, 'admin'); + // CosigningDelegation create node (result of the accept) → admin = receiver + const delegationFields = findCreateNodeFields('CosigningDelegation'); + if (delegationFields) { + const adminData = getField(delegationFields, 'admin'); if (adminData?.oneofKind === 'party') receiver = adminData.party ?? ''; } amount = '0'; diff --git a/modules/sdk-coin-canton/test/resources.ts b/modules/sdk-coin-canton/test/resources.ts index dc8718040f..d1774789a0 100644 --- a/modules/sdk-coin-canton/test/resources.ts +++ b/modules/sdk-coin-canton/test/resources.ts @@ -348,3 +348,14 @@ export const CantonCreateCommandPrepareResponse = { preparedTransactionHash: 'xn2fK57XUY7MFHWAsppKczOkgUYx//0VyMC1jBNbuPI=', hashingSchemeVersion: 'HASHING_SCHEME_VERSION_V2', }; + +export const CosignDelegationAcceptPrepareResponse = { + preparedTransaction: + 'CqcPCgMyLjESATAaygYKATHCPsMGCsAGCgMyLjESQjAwMWJkYTEwYzA4ZTFmNDVlZTVlZTFlMmFjYmUzOTkwNGJhMWJmOTczNmM4MTRiYTgxNDNjMWQyNGY2ZTYyODkwNxoPdHJhZGV3ZWItZGFkLXYxIm4KQDgyYzEwNzVhMzA3NTFlOTEzYTBlNzcxM2M0MDVlNmZjOWVjZGIzNmEwMTI1NDVlMWY4YjMxMDA0MDdiYjQ4ZTMSFVRXLk9wZXJhdG9yLlYxLkNvc2lnbhoTQ29zaWduaW5nRGVsZWdhdGlvbiqtAnKqAgpuCkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aE0Nvc2lnbmluZ0RlbGVnYXRpb24SXwoFYWRtaW4SVjpUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyElcKBnNpZ25lchJNOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzcySzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzJUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzc6VHJhdmktbmV3LXBhcnR5OjoxMjIwOTJlN2QzM2FjMTBjMGYzZDU1OTc2MzQyZjM3NTU1ZGYwNWRhNWI3NDI5NTZkNTZhNjJhZTIzNjc3NjkwNzlkMhqFCAoBMMI+/gca+wcKAzIuMRKKATAwNTBjNjIzMmQzOWU1ZDFlNGQ5MTZiMmRiZTlkM2Q2NGFkZTc0YjQyZDliOTQyODkxNjhiMTM4NmE1NjNkYTU5MWNhMTIxMjIwNTMyMDdhYWFkYmZiYThlYTQ5NGY5NWVhY2IxMTA4YzVhZjc2ZWZiNDk5ODk3MWYyYzljZDc2NTk1NjU3NzA4NBoPdHJhZGV3ZWItZGFkLXYxInYKQDgyYzEwNzVhMzA3NTFlOTEzYTBlNzcxM2M0MDVlNmZjOWVjZGIzNmEwMTI1NDVlMWY4YjMxMDA0MDdiYjQ4ZTMSFVRXLk9wZXJhdG9yLlYxLkNvc2lnbhobQ29zaWduaW5nRGVsZWdhdGlvblByb3Bvc2FsKlRyYXZpLW5ldy1wYXJ0eTo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDIySzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzJUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzdKIkNvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbF9BY2NlcHRSgQFyfwp9CkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aIkNvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbF9BY2NlcHRYAWIBMWrqAXLnAQqEAQpAODJjMTA3NWEzMDc1MWU5MTNhMGU3NzEzYzQwNWU2ZmM5ZWNkYjM2YTAxMjU0NWUxZjhiMzEwMDQwN2JiNDhlMxIVVFcuT3BlcmF0b3IuVjEuQ29zaWduGilDb3NpZ25pbmdEZWxlZ2F0aW9uUHJvcG9zYWxfQWNjZXB0X1Jlc3VsdBJeChZjb3NpZ25pbmdEZWxlZ2F0aW9uQ2lkEkRKQjAwMWJkYTEwYzA4ZTFmNDVlZTVlZTFlMmFjYmUzOTkwNGJhMWJmOTczNmM4MTRiYTgxNDNjMWQyNGY2ZTYyODkwNyIiEiDexREPEP5Xv4zbEY2fCbOcVZi0Df7jF2cc4nnu2wJtsSIkCAESIJs7xUanIWWT1wZipHMBsulvXBnAE6R1amIRgSwCiBjUEr0NEnMKSzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNxIkYTQyOTIyNTQtMzYxMC00ZGMyLTkzZTAtNTkwOGZiOWNjZTFmGlNnbG9iYWwtZG9tYWluOjoxMjIwYmU1OGMyOWU2NWRlNDBiZjI3M2JlMWRjMmIyNjZkNDNhOWEwMDJlYTViMTg5NTVhZWVmN2FhYzg4MWJiNDcxYSokNTBiNWRkODAtYWFhOS00YzJlLTkzNjMtNTUzOTA1NjFlMWU1MKjThsOO/JQDOsELCswGCgMyLjESigEwMDUwYzYyMzJkMzllNWQxZTRkOTE2YjJkYmU5ZDNkNjRhZGU3NGI0MmQ5Yjk0Mjg5MTY4YjEzODZhNTYzZGE1OTFjYTEyMTIyMDUzMjA3YWFhZGJmYmE4ZWE0OTRmOTVlYWNiMTEwOGM1YWY3NmVmYjQ5OTg5NzFmMmM5Y2Q3NjU5NTY1NzcwODQaD3RyYWRld2ViLWRhZC12MSJ2CkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aG0Nvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbCq1AnKyAgp2CkA4MmMxMDc1YTMwNzUxZTkxM2EwZTc3MTNjNDA1ZTZmYzllY2RiMzZhMDEyNTQ1ZTFmOGIzMTAwNDA3YmI0OGUzEhVUVy5PcGVyYXRvci5WMS5Db3NpZ24aG0Nvc2lnbmluZ0RlbGVnYXRpb25Qcm9wb3NhbBJfCgVhZG1pbhJWOlRyYXZpLW5ldy1wYXJ0eTo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDISVwoGc2lnbmVyEk06SzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzJUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyOksxMjIwODo6MTIyMDgzMDgyZTlhZjE1NmZlYWViN2FmZDM2M2EwZWU1ZmZhMWZkMTYwOTQ3YjY0N2ExMzlhN2UwYzJlZDc4ZjVkYzc6VHJhdmktbmV3LXBhcnR5OjoxMjIwOTJlN2QzM2FjMTBjMGYzZDU1OTc2MzQyZjM3NTU1ZGYwNWRhNWI3NDI5NTZkNTZhNjJhZTIzNjc3NjkwNzlkMsA+ttP6pP/7lAPSPuQECgMyLjES3AQKRQBQxiMtOeXR5NkWstvp09ZK3nS0LZuUKJFosThqVj2lkcoSEiBTIHqq2/uo6klPlerLEQjFr3bvtJmJcfLJzXZZVldwhBIPdHJhZGV3ZWItZGFkLXYxGnkKQDgyYzEwNzVhMzA3NTFlOTEzYTBlNzcxM2M0MDVlNmZjOWVjZGIzNmEwMTI1NDVlMWY4YjMxMDA0MDdiYjQ4ZTMSAlRXEghPcGVyYXRvchICVjESBkNvc2lnbhobQ29zaWduaW5nRGVsZWdhdGlvblByb3Bvc2FsIq4BaqsBClgKVjpUcmF2aS1uZXctcGFydHk6OjEyMjA5MmU3ZDMzYWMxMGMwZjNkNTU5NzYzNDJmMzc1NTVkZjA1ZGE1Yjc0Mjk1NmQ1NmE2MmFlMjM2Nzc2OTA3OWQyCk8KTTpLMTIyMDg6OjEyMjA4MzA4MmU5YWYxNTZmZWFlYjdhZmQzNjNhMGVlNWZmYTFmZDE2MDk0N2I2NDdhMTM5YTdlMGMyZWQ3OGY1ZGM3KlRyYXZpLW5ldy1wYXJ0eTo6MTIyMDkyZTdkMzNhYzEwYzBmM2Q1NTk3NjM0MmYzNzU1NWRmMDVkYTViNzQyOTU2ZDU2YTYyYWUyMzY3NzY5MDc5ZDIySzEyMjA4OjoxMjIwODMwODJlOWFmMTU2ZmVhZWI3YWZkMzYzYTBlZTVmZmExZmQxNjA5NDdiNjQ3YTEzOWE3ZTBjMmVkNzhmNWRjNzm2qZ7031MGAEIqCiYKJAgBEiBDS185jpVaGFmJ2vX54nF3ZtTWTXiSUFXL/dL5ESXNlhAe', + preparedTransactionHash: 'd6iTd+VjYA/cJw7pj+CaKVSs0MqMXV9LZNU45Ij1188=', + hashingSchemeVersion: 'HASHING_SCHEME_VERSION_V2', + hashingDetails: null, +}; + +export const CosignDelegationAcceptRawTransaction = + 'eyJwcmVwYXJlQ29tbWFuZFJlc3BvbnNlIjp7InByZXBhcmVkVHJhbnNhY3Rpb24iOiJDcWNQQ2dNeUxqRVNBVEFheWdZS0FUSENQc01HQ3NBR0NnTXlMakVTUWpBd01XSmtZVEV3WXpBNFpURm1ORFZsWlRWbFpURmxNbUZqWW1Vek9Ua3dOR0poTVdKbU9UY3pObU00TVRSaVlUZ3hORE5qTVdReU5HWTJaVFl5T0Rrd054b1BkSEpoWkdWM1pXSXRaR0ZrTFhZeEltNEtRRGd5WXpFd056VmhNekEzTlRGbE9URXpZVEJsTnpjeE0yTTBNRFZsTm1aak9XVmpaR0l6Tm1Fd01USTFORFZsTVdZNFlqTXhNREEwTURkaVlqUTRaVE1TRlZSWExrOXdaWEpoZEc5eUxsWXhMa052YzJsbmJob1RRMjl6YVdkdWFXNW5SR1ZzWldkaGRHbHZiaXF0QW5LcUFncHVDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFFME52YzJsbmJtbHVaMFJsYkdWbllYUnBiMjRTWHdvRllXUnRhVzRTVmpwVWNtRjJhUzF1WlhjdGNHRnlkSGs2T2pFeU1qQTVNbVUzWkRNellXTXhNR013WmpOa05UVTVOell6TkRKbU16YzFOVFZrWmpBMVpHRTFZamMwTWprMU5tUTFObUUyTW1GbE1qTTJOemMyT1RBM09XUXlFbGNLQm5OcFoyNWxjaEpOT2tzeE1qSXdPRG82TVRJeU1EZ3pNRGd5WlRsaFpqRTFObVpsWVdWaU4yRm1aRE0yTTJFd1pXVTFabVpoTVdaa01UWXdPVFEzWWpZME4yRXhNemxoTjJVd1l6SmxaRGM0WmpWa1l6Y3lTekV5TWpBNE9qb3hNakl3T0RNd09ESmxPV0ZtTVRVMlptVmhaV0kzWVdaa016WXpZVEJsWlRWbVptRXhabVF4TmpBNU5EZGlOalEzWVRFek9XRTNaVEJqTW1Wa056aG1OV1JqTnpKVWNtRjJhUzF1WlhjdGNHRnlkSGs2T2pFeU1qQTVNbVUzWkRNellXTXhNR013WmpOa05UVTVOell6TkRKbU16YzFOVFZrWmpBMVpHRTFZamMwTWprMU5tUTFObUUyTW1GbE1qTTJOemMyT1RBM09XUXlPa3N4TWpJd09EbzZNVEl5TURnek1EZ3laVGxoWmpFMU5tWmxZV1ZpTjJGbVpETTJNMkV3WldVMVptWmhNV1prTVRZd09UUTNZalkwTjJFeE16bGhOMlV3WXpKbFpEYzRaalZrWXpjNlZISmhkbWt0Ym1WM0xYQmhjblI1T2pveE1qSXdPVEpsTjJRek0yRmpNVEJqTUdZelpEVTFPVGMyTXpReVpqTTNOVFUxWkdZd05XUmhOV0kzTkRJNU5UWmtOVFpoTmpKaFpUSXpOamMzTmprd056bGtNaHFGQ0FvQk1NSSsvZ2NhK3djS0F6SXVNUktLQVRBd05UQmpOakl6TW1Rek9XVTFaREZsTkdRNU1UWmlNbVJpWlRsa00yUTJOR0ZrWlRjMFlqUXlaRGxpT1RReU9Ea3hOamhpTVRNNE5tRTFOak5rWVRVNU1XTmhNVEl4TWpJd05UTXlNRGRoWVdGa1ltWmlZVGhsWVRRNU5HWTVOV1ZoWTJJeE1UQTRZelZoWmpjMlpXWmlORGs1T0RrM01XWXlZemxqWkRjMk5UazFOalUzTnpBNE5Cb1BkSEpoWkdWM1pXSXRaR0ZrTFhZeEluWUtRRGd5WXpFd056VmhNekEzTlRGbE9URXpZVEJsTnpjeE0yTTBNRFZsTm1aak9XVmpaR0l6Tm1Fd01USTFORFZsTVdZNFlqTXhNREEwTURkaVlqUTRaVE1TRlZSWExrOXdaWEpoZEc5eUxsWXhMa052YzJsbmJob2JRMjl6YVdkdWFXNW5SR1ZzWldkaGRHbHZibEJ5YjNCdmMyRnNLbFJ5WVhacExXNWxkeTF3WVhKMGVUbzZNVEl5TURreVpUZGtNek5oWXpFd1l6Qm1NMlExTlRrM05qTTBNbVl6TnpVMU5XUm1NRFZrWVRWaU56UXlPVFUyWkRVMllUWXlZV1V5TXpZM056WTVNRGM1WkRJeVN6RXlNakE0T2pveE1qSXdPRE13T0RKbE9XRm1NVFUyWm1WaFpXSTNZV1prTXpZellUQmxaVFZtWm1FeFptUXhOakE1TkRkaU5qUTNZVEV6T1dFM1pUQmpNbVZrTnpobU5XUmpOekpVY21GMmFTMXVaWGN0Y0dGeWRIazZPakV5TWpBNU1tVTNaRE16WVdNeE1HTXdaak5rTlRVNU56WXpOREptTXpjMU5UVmtaakExWkdFMVlqYzBNamsxTm1RMU5tRTJNbUZsTWpNMk56YzJPVEEzT1dReU9rc3hNakl3T0RvNk1USXlNRGd6TURneVpUbGhaakUxTm1abFlXVmlOMkZtWkRNMk0yRXdaV1UxWm1aaE1XWmtNVFl3T1RRM1lqWTBOMkV4TXpsaE4yVXdZekpsWkRjNFpqVmtZemRLSWtOdmMybG5ibWx1WjBSbGJHVm5ZWFJwYjI1UWNtOXdiM05oYkY5QlkyTmxjSFJTZ1FGeWZ3cDlDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFJa052YzJsbmJtbHVaMFJsYkdWbllYUnBiMjVRY205d2IzTmhiRjlCWTJObGNIUllBV0lCTVdycUFYTG5BUXFFQVFwQU9ESmpNVEEzTldFek1EYzFNV1U1TVROaE1HVTNOekV6WXpRd05XVTJabU01WldOa1lqTTJZVEF4TWpVME5XVXhaamhpTXpFd01EUXdOMkppTkRobE14SVZWRmN1VDNCbGNtRjBiM0l1VmpFdVEyOXphV2R1R2lsRGIzTnBaMjVwYm1kRVpXeGxaMkYwYVc5dVVISnZjRzl6WVd4ZlFXTmpaWEIwWDFKbGMzVnNkQkplQ2haamIzTnBaMjVwYm1kRVpXeGxaMkYwYVc5dVEybGtFa1JLUWpBd01XSmtZVEV3WXpBNFpURm1ORFZsWlRWbFpURmxNbUZqWW1Vek9Ua3dOR0poTVdKbU9UY3pObU00TVRSaVlUZ3hORE5qTVdReU5HWTJaVFl5T0Rrd055SWlFaURleFJFUEVQNVh2NHpiRVkyZkNiT2NWWmkwRGY3akYyY2M0bm51MndKdHNTSWtDQUVTSUpzN3hVYW5JV1dUMXdaaXBITUJzdWx2WEJuQUU2UjFhbUlSZ1N3Q2lCalVFcjBORW5NS1N6RXlNakE0T2pveE1qSXdPRE13T0RKbE9XRm1NVFUyWm1WaFpXSTNZV1prTXpZellUQmxaVFZtWm1FeFptUXhOakE1TkRkaU5qUTNZVEV6T1dFM1pUQmpNbVZrTnpobU5XUmpOeElrWVRReU9USXlOVFF0TXpZeE1DMDBaR015TFRrelpUQXROVGt3T0daaU9XTmpaVEZtR2xObmJHOWlZV3d0Wkc5dFlXbHVPam94TWpJd1ltVTFPR015T1dVMk5XUmxOREJpWmpJM00ySmxNV1JqTW1JeU5qWmtORE5oT1dFd01ESmxZVFZpTVRnNU5UVmhaV1ZtTjJGaFl6ZzRNV0ppTkRjeFlTb2tOVEJpTldSa09EQXRZV0ZoT1MwMFl6SmxMVGt6TmpNdE5UVXpPVEExTmpGbE1XVTFNS2pUaHNPTy9KUURPc0VMQ3N3R0NnTXlMakVTaWdFd01EVXdZell5TXpKa016bGxOV1F4WlRSa09URTJZakprWW1VNVpETmtOalJoWkdVM05HSTBNbVE1WWprME1qZzVNVFk0WWpFek9EWmhOVFl6WkdFMU9URmpZVEV5TVRJeU1EVXpNakEzWVdGaFpHSm1ZbUU0WldFME9UUm1PVFZsWVdOaU1URXdPR00xWVdZM05tVm1ZalE1T1RnNU56Rm1NbU01WTJRM05qVTVOVFkxTnpjd09EUWFEM1J5WVdSbGQyVmlMV1JoWkMxMk1TSjJDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFHME52YzJsbmJtbHVaMFJsYkdWbllYUnBiMjVRY205d2IzTmhiQ3ExQW5LeUFncDJDa0E0TW1NeE1EYzFZVE13TnpVeFpUa3hNMkV3WlRjM01UTmpOREExWlRabVl6bGxZMlJpTXpaaE1ERXlOVFExWlRGbU9HSXpNVEF3TkRBM1ltSTBPR1V6RWhWVVZ5NVBjR1Z5WVhSdmNpNVdNUzVEYjNOcFoyNGFHME52YzJsbmJtbHVaMFJsYkdWbllYUnBiMjVRY205d2IzTmhiQkpmQ2dWaFpHMXBiaEpXT2xSeVlYWnBMVzVsZHkxd1lYSjBlVG82TVRJeU1Ea3laVGRrTXpOaFl6RXdZekJtTTJRMU5UazNOak0wTW1Zek56VTFOV1JtTURWa1lUVmlOelF5T1RVMlpEVTJZVFl5WVdVeU16WTNOelk1TURjNVpESVNWd29HYzJsbmJtVnlFazA2U3pFeU1qQTRPam94TWpJd09ETXdPREpsT1dGbU1UVTJabVZoWldJM1lXWmtNell6WVRCbFpUVm1abUV4Wm1ReE5qQTVORGRpTmpRM1lURXpPV0UzWlRCak1tVmtOemhtTldSak56SlVjbUYyYVMxdVpYY3RjR0Z5ZEhrNk9qRXlNakE1TW1VM1pETXpZV014TUdNd1pqTmtOVFU1TnpZek5ESm1NemMxTlRWa1pqQTFaR0UxWWpjME1qazFObVExTm1FMk1tRmxNak0yTnpjMk9UQTNPV1F5T2tzeE1qSXdPRG82TVRJeU1EZ3pNRGd5WlRsaFpqRTFObVpsWVdWaU4yRm1aRE0yTTJFd1pXVTFabVpoTVdaa01UWXdPVFEzWWpZME4yRXhNemxoTjJVd1l6SmxaRGM0WmpWa1l6YzZWSEpoZG1rdGJtVjNMWEJoY25SNU9qb3hNakl3T1RKbE4yUXpNMkZqTVRCak1HWXpaRFUxT1RjMk16UXlaak0zTlRVMVpHWXdOV1JoTldJM05ESTVOVFprTlRaaE5qSmhaVEl6TmpjM05qa3dOemxrTXNBK3R0UDZwUC83bEFQU1B1UUVDZ015TGpFUzNBUUtSUUJReGlNdE9lWFI1TmtXc3R2cDA5WkszblMwTFp1VUtKRm9zVGhxVmoybGtjb1NFaUJUSUhxcTIvdW82a2xQbGVyTEVRakZyM2J2dEptSmNmTEp6WFpaVmxkd2hCSVBkSEpoWkdWM1pXSXRaR0ZrTFhZeEdua0tRRGd5WXpFd056VmhNekEzTlRGbE9URXpZVEJsTnpjeE0yTTBNRFZsTm1aak9XVmpaR0l6Tm1Fd01USTFORFZsTVdZNFlqTXhNREEwTURkaVlqUTRaVE1TQWxSWEVnaFBjR1Z5WVhSdmNoSUNWakVTQmtOdmMybG5iaG9iUTI5emFXZHVhVzVuUkdWc1pXZGhkR2x2YmxCeWIzQnZjMkZzSXE0QmFxc0JDbGdLVmpwVWNtRjJhUzF1WlhjdGNHRnlkSGs2T2pFeU1qQTVNbVUzWkRNellXTXhNR013WmpOa05UVTVOell6TkRKbU16YzFOVFZrWmpBMVpHRTFZamMwTWprMU5tUTFObUUyTW1GbE1qTTJOemMyT1RBM09XUXlDazhLVFRwTE1USXlNRGc2T2pFeU1qQTRNekE0TW1VNVlXWXhOVFptWldGbFlqZGhabVF6TmpOaE1HVmxOV1ptWVRGbVpERTJNRGswTjJJMk5EZGhNVE01WVRkbE1HTXlaV1EzT0dZMVpHTTNLbFJ5WVhacExXNWxkeTF3WVhKMGVUbzZNVEl5TURreVpUZGtNek5oWXpFd1l6Qm1NMlExTlRrM05qTTBNbVl6TnpVMU5XUm1NRFZrWVRWaU56UXlPVFUyWkRVMllUWXlZV1V5TXpZM056WTVNRGM1WkRJeVN6RXlNakE0T2pveE1qSXdPRE13T0RKbE9XRm1NVFUyWm1WaFpXSTNZV1prTXpZellUQmxaVFZtWm1FeFptUXhOakE1TkRkaU5qUTNZVEV6T1dFM1pUQmpNbVZrTnpobU5XUmpOem0ycVo3MDMxTUdBRUlxQ2lZS0pBZ0JFaUJEUzE4NWpwVmFHRm1KMnZYNTRuRjNadFRXVFhpU1VGWEwvZEw1RVNYTmxoQWUiLCJwcmVwYXJlZFRyYW5zYWN0aW9uSGFzaCI6ImQ2aVRkK1ZqWUEvY0p3N3BqK0NhS1ZTczBNcU1YVjlMWk5VNDVJajExODg9IiwiaGFzaGluZ1NjaGVtZVZlcnNpb24iOiJIQVNISU5HX1NDSEVNRV9WRVJTSU9OX1YyIiwiaGFzaGluZ0RldGFpbHMiOm51bGx9LCJ0eFR5cGUiOiJDb3NpZ25EZWxlZ2F0aW9uQWNjZXB0IiwicHJlcGFyZWRUcmFuc2FjdGlvbiI6IiIsInBhcnR5U2lnbmF0dXJlcyI6eyJzaWduYXR1cmVzIjpbXX0sImRlZHVwbGljYXRpb25QZXJpb2QiOnsiRW1wdHkiOnt9fSwiaGFzaGluZ1NjaGVtZVZlcnNpb24iOiJIQVNISU5HX1NDSEVNRV9WRVJTSU9OX1YyIiwibWluTGVkZ2VyVGltZSI6eyJ0aW1lIjp7IkVtcHR5Ijp7fX19LCJzdWJtaXNzaW9uSWQiOiIzOTM1YTA2ZC0zYjAzLTQxYmUtOTlhNS05NWIyZWNhYWJmN2QifQ=='; diff --git a/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts b/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts index dacc45e32c..08ec8b8c4e 100644 --- a/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts +++ b/modules/sdk-coin-canton/test/unit/builder/cosignDelegationAccept/cosignDelegationAcceptBuilder.ts @@ -3,8 +3,9 @@ import should from 'should'; import { coins } from '@bitgo/statics'; -import { CosignDelegationAcceptBuilder, Transaction } from '../../../../src'; -import { CantonTransferAcceptRejectRequest } from '../../../../src/lib/iface'; +import { CosignDelegationAcceptBuilder, Transaction, TransactionBuilderFactory } from '../../../../src'; +import { CantonTransferAcceptRejectRequest, TxData } from '../../../../src/lib/iface'; +import { CosignDelegationAcceptPrepareResponse, CosignDelegationAcceptRawTransaction } from '../../../resources'; const commandId = '3935a06d-3b03-41be-99a5-95b2ecaabf7d'; const contractId = @@ -79,4 +80,41 @@ describe('CosignDelegationAccept Builder', () => { txBuilder.initBuilder(tx); assert.throws(() => txBuilder.actAs(''), /actAsPartyId must be a non-empty string/); }); + + it('should parse preparedTransaction and extract sender, receiver, and amount via setTransaction + toJson', function () { + const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + txBuilder.setTransaction(CosignDelegationAcceptPrepareResponse); + txBuilder.commandId(commandId).contractId(contractId).actAs(actAsPartyId); + const txData = txBuilder.transaction.toJson() as TxData; + should.exist(txData); + assert.equal(txData.sender, '12208::122083082e9af156feaeb7afd363a0ee5ffa1fd160947b647a139a7e0c2ed78f5dc7'); + assert.equal( + txData.receiver, + 'ravi-new-party::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2' + ); + assert.equal(txData.amount, '0'); + }); + + it('should parse round-trip from raw transaction via TransactionBuilderFactory.from', function () { + const factory = new TransactionBuilderFactory(coins.get('tcanton')); + const txBuilder = factory.from(CosignDelegationAcceptRawTransaction); + const txData = txBuilder.transaction.toJson() as TxData; + should.exist(txData); + assert.equal(txData.sender, '12208::122083082e9af156feaeb7afd363a0ee5ffa1fd160947b647a139a7e0c2ed78f5dc7'); + assert.equal( + txData.receiver, + 'ravi-new-party::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2' + ); + assert.equal(txData.amount, '0'); + }); + + it('should validate raw transaction hash', function () { + const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + txBuilder.setTransaction(CosignDelegationAcceptPrepareResponse); + txBuilder.validateRawTransaction(CosignDelegationAcceptPrepareResponse.preparedTransaction); + }); }); diff --git a/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts b/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts index e6b68f3ac3..ba34e2333b 100644 --- a/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts +++ b/modules/sdk-coin-near/test/unit/tokenEnablementValidation.ts @@ -612,6 +612,57 @@ describe('NEAR Token Enablement Validation', function () { ); }); + it('should populate recipients from enableTokens in buildParams for TSS wallets', async function () { + // Regression test: before the fix, buildTokenEnablements on TSS wallets did not populate + // buildParams.recipients — only buildParams.enableTokens was set. This caused verifyTransaction + // to throw "missing token name in transaction parameters" because it reads from + // txParams.recipients[0].tokenName (where txParams = { ...txPrebuild.buildParams, ...params }). + const bgUrl = common.Environments['test'].uri; + + nock(bgUrl) + .post(`/api/v2/wallet/${tssWallet.id()}/txrequests`) + .reply(200, { + txRequestId: 'test-request-id', + apiVersion: 'full', + transactions: [ + { + state: 'pending', + unsignedTx: { + serializedTxHex: testData.rawTx.selfStorageDeposit.unsigned, + signableHex: testData.rawTx.selfStorageDeposit.unsigned, + derivationPath: 'm/0', + feeInfo: { fee: 1160407, feeString: '1160407' }, + }, + signatureShares: [], + }, + ], + }); + + const buildResult = await tssWallet.buildTokenEnablements({ + enableTokens: [{ name: 'tnear:tnep24dp' }], + }); + + const txPrebuild = buildResult[0] as any; + + // Verify buildParams.recipients is populated — this is what flows into txParams + // via { ...txPrebuild.buildParams, ...params } inside prebuildAndSignTransaction + txPrebuild.buildParams.should.have.property('recipients'); + txPrebuild.buildParams.recipients.should.have.length(1); + txPrebuild.buildParams.recipients[0].tokenName.should.equal('tnear:tnep24dp'); + txPrebuild.buildParams.recipients[0].address.should.equal(testData.accounts.account1.address); + + // Simulate the txParams construction that prebuildAndSignTransaction performs, + // then confirm verifyTransaction no longer throws "missing token name" + const txParams = { ...txPrebuild.buildParams }; + await basecoin.verifyTransaction({ + txParams, + txPrebuild, + wallet: tssWallet as any, + verification: { verifyTokenEnablement: true }, + walletType: 'tss', + }); + }); + it('should validate correct storage deposit in TSS wallet flow', async function () { const bgUrl = common.Environments['test'].uri; diff --git a/modules/sdk-coin-starknet/src/lib/constants.ts b/modules/sdk-coin-starknet/src/lib/constants.ts index 877b83c45a..398cdd249f 100644 --- a/modules/sdk-coin-starknet/src/lib/constants.ts +++ b/modules/sdk-coin-starknet/src/lib/constants.ts @@ -23,6 +23,9 @@ export const DEFAULT_SEED_SIZE_BYTES = 16; // V3 transaction hash prefix: encodeShortString("invoke") export const INVOKE_TX_PREFIX = 0x696e766f6b65n; +// V3 transaction hash prefix: encodeShortString("deploy_account") +export const DEPLOY_ACCOUNT_TX_PREFIX = 0x6465706c6f795f6163636f756e74n; + // V3 transaction version export const TRANSACTION_VERSION_3 = 3n; diff --git a/modules/sdk-coin-starknet/src/lib/iface.ts b/modules/sdk-coin-starknet/src/lib/iface.ts index 98c8746218..3614a410ae 100644 --- a/modules/sdk-coin-starknet/src/lib/iface.ts +++ b/modules/sdk-coin-starknet/src/lib/iface.ts @@ -34,6 +34,12 @@ export interface StarknetTransactionData { nonceDataAvailabilityMode?: number; feeDataAvailabilityMode?: number; compiledCalldata?: string[]; + /** DEPLOY_ACCOUNT: OZ EthAccount class hash. */ + classHash?: string; + /** DEPLOY_ACCOUNT: constructor calldata (pubkey limbs). */ + constructorCalldata?: string[]; + /** DEPLOY_ACCOUNT: address salt derived from pubkey. */ + contractAddressSalt?: string; } export interface InvokeTransactionHashParams { @@ -50,6 +56,20 @@ export interface InvokeTransactionHashParams { proofFacts?: string[]; } +export interface DeployAccountTransactionHashParams { + contractAddress: string; + classHash: string; + constructorCalldata: string[]; + contractAddressSalt: string; + chainId: string; + nonce: string; + resourceBounds: StarknetResourceBounds; + tip?: string; + nonceDataAvailabilityMode?: number; + feeDataAvailabilityMode?: number; + paymasterData?: string[]; +} + export interface ParsedTransferData { recipient: string; amount: string; diff --git a/modules/sdk-coin-starknet/src/lib/index.ts b/modules/sdk-coin-starknet/src/lib/index.ts index 839446d4bd..047e8281a1 100644 --- a/modules/sdk-coin-starknet/src/lib/index.ts +++ b/modules/sdk-coin-starknet/src/lib/index.ts @@ -4,6 +4,7 @@ export * from './iface'; export { KeyPair } from './keyPair'; export { TransactionBuilder } from './transactionBuilder'; export { TransferBuilder } from './transferBuilder'; +export { WalletInitializationBuilder } from './walletInitializationBuilder'; export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { Transaction } from './transaction'; export { Utils }; diff --git a/modules/sdk-coin-starknet/src/lib/transaction.ts b/modules/sdk-coin-starknet/src/lib/transaction.ts index d18ee28a55..95466f8c45 100644 --- a/modules/sdk-coin-starknet/src/lib/transaction.ts +++ b/modules/sdk-coin-starknet/src/lib/transaction.ts @@ -80,6 +80,9 @@ export class Transaction extends BaseTransaction { compiledCalldata: parsed.compiledCalldata, nonceDataAvailabilityMode: parsed.nonceDataAvailabilityMode, feeDataAvailabilityMode: parsed.feeDataAvailabilityMode, + classHash: parsed.classHash, + constructorCalldata: parsed.constructorCalldata, + contractAddressSalt: parsed.contractAddressSalt, }; if (parsed.signature && parsed.signature.length > 0) { @@ -146,13 +149,44 @@ export class Transaction extends BaseTransaction { return Buffer.from(JSON.stringify(data), 'utf-8').toString('hex'); } - /** @inheritdoc — returns Starknet RPC-ready JSON string for starknet_addInvokeTransaction. */ + /** @inheritdoc — returns Starknet RPC-ready JSON for addInvoke or addDeployAccount. */ toBroadcastFormat(): string { const data = this._starknetTransactionData; if (!data) { throw new InvalidTransactionError('Empty transaction'); } - return JSON.stringify({ + + const payload = + data.transactionType === StarknetTransactionType.DEPLOY_ACCOUNT + ? this.buildDeployAccountPayload(data) + : this.buildInvokePayload(data); + + return JSON.stringify(payload); + } + private buildDeployAccountPayload(data: StarknetTransactionData) { + if (!data.classHash || !data.constructorCalldata || !data.contractAddressSalt) { + throw new InvalidTransactionError('Incomplete deploy account transaction'); + } + + return { + type: 'DEPLOY_ACCOUNT', + version: '0x3', + signature: data.signature || [], + nonce: data.nonce, + contract_address_salt: data.contractAddressSalt, + constructor_calldata: data.constructorCalldata, + class_hash: data.classHash, + sender_address: data.senderAddress, + resource_bounds: resolveResourceBounds(data), + tip: data.tip || '0x0', + paymaster_data: [], + nonce_data_availability_mode: 'L1', + fee_data_availability_mode: 'L1', + }; + } + + private buildInvokePayload(data: StarknetTransactionData) { + return { type: 'INVOKE', version: '0x3', sender_address: data.senderAddress, @@ -165,7 +199,7 @@ export class Transaction extends BaseTransaction { account_deployment_data: [], nonce_data_availability_mode: 'L1', fee_data_availability_mode: 'L1', - }); + }; } /** @inheritdoc */ diff --git a/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts index de691811d8..e1916cff4f 100644 --- a/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-starknet/src/lib/transactionBuilderFactory.ts @@ -1,8 +1,9 @@ -import { BaseTransactionBuilderFactory, InvalidTransactionError, MethodNotImplementedError } from '@bitgo/sdk-core'; +import { BaseTransactionBuilderFactory, InvalidTransactionError } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { Transaction } from './transaction'; import { TransactionBuilder } from './transactionBuilder'; import { TransferBuilder } from './transferBuilder'; +import { WalletInitializationBuilder } from './walletInitializationBuilder'; import { StarknetTransactionType } from './iface'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { @@ -18,6 +19,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { switch (transaction.starknetTransactionData.transactionType) { case StarknetTransactionType.INVOKE: return this.getTransferBuilder(transaction); + case StarknetTransactionType.DEPLOY_ACCOUNT: + return this.getWalletInitializationBuilder(transaction); default: throw new InvalidTransactionError('Invalid transaction type'); } @@ -39,7 +42,7 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { } /** @inheritdoc */ - getWalletInitializationBuilder(): void { - throw new MethodNotImplementedError(); + getWalletInitializationBuilder(tx?: Transaction): WalletInitializationBuilder { + return TransactionBuilderFactory.initializeBuilder(tx, new WalletInitializationBuilder(this._coinConfig)); } } diff --git a/modules/sdk-coin-starknet/src/lib/utils.ts b/modules/sdk-coin-starknet/src/lib/utils.ts index 6883bfc9d8..9c78750abd 100644 --- a/modules/sdk-coin-starknet/src/lib/utils.ts +++ b/modules/sdk-coin-starknet/src/lib/utils.ts @@ -6,12 +6,20 @@ import { ADDR_BOUND, CONTRACT_ADDRESS_PREFIX, INVOKE_TX_PREFIX, + DEPLOY_ACCOUNT_TX_PREFIX, TRANSACTION_VERSION_3, L1_GAS_NAME, L2_GAS_NAME, L1_DATA_GAS_NAME, } from './constants'; -import { StarknetTransactionData, StarknetCall, ParsedTransferData, InvokeTransactionHashParams } from './iface'; +import { + StarknetTransactionData, + StarknetTransactionType, + StarknetCall, + ParsedTransferData, + InvokeTransactionHashParams, + DeployAccountTransactionHashParams, +} from './iface'; import { ecc } from '@bitgo/secp256k1'; /** @@ -207,6 +215,17 @@ export function validateRawTransaction(tx: StarknetTransactionData): void { if (!isValidAddress(tx.senderAddress)) { throw new Error(`Invalid sender address: ${tx.senderAddress}`); } + if (tx.transactionType === StarknetTransactionType.DEPLOY_ACCOUNT) { + if (!tx.classHash) { + throw new Error('Missing class hash for deploy account transaction'); + } + if (!tx.constructorCalldata || tx.constructorCalldata.length === 0) { + throw new Error('Missing constructor calldata for deploy account transaction'); + } + if (!tx.contractAddressSalt) { + throw new Error('Missing contract address salt for deploy account transaction'); + } + } } /** @@ -309,6 +328,55 @@ export function calculateInvokeTransactionHash(params: InvokeTransactionHashPara return '0x' + hash.toString(16); } +/** + * Compute the Poseidon V3 DEPLOY_ACCOUNT transaction hash per SNIP-8 (starknet.js v3). + */ +export function calculateDeployAccountTransactionHash(params: DeployAccountTransactionHashParams): string { + const { + contractAddress, + classHash, + constructorCalldata, + contractAddressSalt, + chainId, + nonce, + resourceBounds, + tip = '0x0', + nonceDataAvailabilityMode = 0, + feeDataAvailabilityMode = 0, + paymasterData = [], + } = params; + + const feeFieldHash = poseidonHashMany([ + BigInt(tip), + encodeResourceBound(L1_GAS_NAME, resourceBounds.l1_gas.max_amount, resourceBounds.l1_gas.max_price_per_unit), + encodeResourceBound(L2_GAS_NAME, resourceBounds.l2_gas.max_amount, resourceBounds.l2_gas.max_price_per_unit), + encodeResourceBound( + L1_DATA_GAS_NAME, + resourceBounds.l1_data_gas.max_amount, + resourceBounds.l1_data_gas.max_price_per_unit + ), + ]); + + const daMode = (BigInt(nonceDataAvailabilityMode) << 32n) | BigInt(feeDataAvailabilityMode); + + const hashFields: bigint[] = [ + DEPLOY_ACCOUNT_TX_PREFIX, + TRANSACTION_VERSION_3, + BigInt(contractAddress), + feeFieldHash, + poseidonHashMany(paymasterData.map(BigInt)), + BigInt(chainId), + BigInt(nonce), + daMode, + poseidonHashMany(constructorCalldata.map(BigInt)), + BigInt(classHash), + BigInt(contractAddressSalt), + ]; + + const hash = poseidonHashMany(hashFields); + return '0x' + hash.toString(16); +} + export default { isValidAddress, isValidPublicKey, @@ -327,4 +395,5 @@ export default { getSelectorFromName, compileExecuteCalldata, calculateInvokeTransactionHash, + calculateDeployAccountTransactionHash, }; diff --git a/modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts b/modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts new file mode 100644 index 0000000000..85555aa0b9 --- /dev/null +++ b/modules/sdk-coin-starknet/src/lib/walletInitializationBuilder.ts @@ -0,0 +1,130 @@ +import { BuildTransactionError } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction'; +import { StarknetTransactionData, StarknetTransactionType } from './iface'; +import { OZ_ETH_ACCOUNT_CLASS_HASH } from './constants'; +import utils from './utils'; + +/** + * Builds DEPLOY_ACCOUNT v3 transactions for counterfactual EthAccount activation. + */ +export class WalletInitializationBuilder extends TransactionBuilder { + protected _classHash: string = OZ_ETH_ACCOUNT_CLASS_HASH; + protected _constructorCalldata?: string[]; + protected _contractAddressSalt?: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + protected get transactionType(): StarknetTransactionType { + return StarknetTransactionType.DEPLOY_ACCOUNT; + } + + /** + * Set deploy parameters from a secp256k1 public key (compressed or uncompressed). + * Derives counterfactual address, constructor calldata, and salt. + */ + public fromPublicKey(pubKey: string): this { + if (!utils.isValidPublicKey(pubKey)) { + throw new BuildTransactionError('Invalid pubKey, got: ' + pubKey); + } + const fullPublicKey = utils.getUncompressedPublicKey(pubKey); + const { address, constructorCalldata, salt } = utils.computeStarknetAddress(fullPublicKey); + this._constructorCalldata = constructorCalldata; + this._contractAddressSalt = salt; + return this.sender(address, pubKey); + } + + public classHash(classHash: string): this { + if (!classHash || !utils.isValidAddress(classHash)) { + throw new BuildTransactionError('Invalid class hash, got: ' + classHash); + } + this._classHash = classHash; + return this; + } + + /** @inheritdoc */ + initBuilder(tx: Transaction): void { + super.initBuilder(tx); + const data = tx.starknetTransactionData; + if (data.classHash) { + this._classHash = data.classHash; + } + if (data.constructorCalldata) { + this._constructorCalldata = data.constructorCalldata; + } + if (data.contractAddressSalt) { + this._contractAddressSalt = data.contractAddressSalt; + } + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + this.ensureDeployFields(); + + const contractAddress = this._sender as string; + const chainId = this._chainId as string; + const nonce = this._nonce as string; + const constructorCalldata = this._constructorCalldata as string[]; + const contractAddressSalt = this._contractAddressSalt as string; + + const transactionHash = utils.calculateDeployAccountTransactionHash({ + contractAddress, + classHash: this._classHash, + constructorCalldata, + contractAddressSalt, + chainId, + nonce, + resourceBounds: this._resourceBounds, + tip: this._tip, + }); + + const data: StarknetTransactionData = { + senderAddress: contractAddress, + calls: [], + nonce, + chainId, + transactionType: StarknetTransactionType.DEPLOY_ACCOUNT, + resourceBounds: this._resourceBounds, + tip: this._tip, + transactionHash, + classHash: this._classHash, + constructorCalldata, + contractAddressSalt, + }; + + this._transaction.starknetTransactionData = data; + return this._transaction; + } + + private ensureDeployFields(): void { + if (!this._sender) { + throw new BuildTransactionError('Sender (counterfactual address) is required'); + } + if (this._publicKey && (!this._constructorCalldata || !this._contractAddressSalt)) { + this.fromPublicKey(this._publicKey); + } + if (!this._constructorCalldata || !this._contractAddressSalt) { + throw new BuildTransactionError( + 'Deploy account requires public key (fromPublicKey) or explicit constructor calldata and salt' + ); + } + const fullPublicKey = this._publicKey ? utils.getUncompressedPublicKey(this._publicKey) : undefined; + if (fullPublicKey) { + const derived = utils.computeStarknetAddress(fullPublicKey); + if (utils.normalizeAddress(derived.address) !== utils.normalizeAddress(this._sender)) { + throw new BuildTransactionError( + `Address does not match public key. Expected ${derived.address}, got ${this._sender}` + ); + } + if ( + derived.constructorCalldata.join(',') !== this._constructorCalldata.join(',') || + derived.salt !== this._contractAddressSalt + ) { + throw new BuildTransactionError('Constructor calldata or salt does not match public key'); + } + } + } +} diff --git a/modules/sdk-coin-starknet/test/unit/utils.ts b/modules/sdk-coin-starknet/test/unit/utils.ts index 3b1565764a..d6db407750 100644 --- a/modules/sdk-coin-starknet/test/unit/utils.ts +++ b/modules/sdk-coin-starknet/test/unit/utils.ts @@ -3,7 +3,10 @@ import utils, { getSelectorFromName, compileExecuteCalldata, calculateInvokeTransactionHash, + calculateDeployAccountTransactionHash, } from '../../src/lib/utils'; +import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; import { Accounts, SandboxTransferData, KnownGoodInvokeTx } from '../resources/starknet'; import { MASK_128 } from '../../src/lib/constants'; import 'should'; @@ -263,4 +266,44 @@ describe('Starknet Utils', () => { hash.should.equal(tv.expectedTxHash); }); }); + + describe('calculateDeployAccountTransactionHash', () => { + it('should be deterministic for the same deploy inputs', () => { + const fullPub = utils.getUncompressedPublicKey(Accounts.account1.publicKey); + const { address, constructorCalldata, salt } = utils.computeStarknetAddress(fullPub); + const params = { + contractAddress: address, + classHash: '0x3940bc18abf1df6bc540cabadb1cad9486c6803b95801e57b6153ae21abfe06', + constructorCalldata, + contractAddressSalt: salt, + chainId: SandboxTransferData.chainId, + nonce: '0x0', + resourceBounds: SandboxTransferData.resourceBounds, + tip: '0x0', + }; + const hash1 = calculateDeployAccountTransactionHash(params); + const hash2 = calculateDeployAccountTransactionHash(params); + hash1.should.equal(hash2); + hash1.should.startWith('0x'); + }); + + it('should match hash from WalletInitializationBuilder build', async () => { + const factory = new TransactionBuilderFactory(coins.get('starknet')); + const builder = factory.getWalletInitializationBuilder(); + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(SandboxTransferData.chainId); + const tx = (await builder.build()) as import('../../src/lib/transaction').Transaction; + const fullPub = utils.getUncompressedPublicKey(Accounts.account1.publicKey); + const { address, constructorCalldata, salt } = utils.computeStarknetAddress(fullPub); + const hash = calculateDeployAccountTransactionHash({ + contractAddress: address, + classHash: '0x3940bc18abf1df6bc540cabadb1cad9486c6803b95801e57b6153ae21abfe06', + constructorCalldata, + contractAddressSalt: salt, + chainId: SandboxTransferData.chainId, + nonce: '0x0', + resourceBounds: SandboxTransferData.resourceBounds, + }); + hash.should.equal(tx.starknetTransactionData.transactionHash); + }); + }); }); diff --git a/modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts b/modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts new file mode 100644 index 0000000000..0998a67c11 --- /dev/null +++ b/modules/sdk-coin-starknet/test/unit/walletInitializationBuilder.ts @@ -0,0 +1,146 @@ +import should from 'should'; +import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; +import { Transaction } from '../../src/lib/transaction'; +import { StarknetTransactionType } from '../../src/lib/iface'; +import { Accounts, SandboxTransferData } from '../resources/starknet'; +import { OZ_ETH_ACCOUNT_CLASS_HASH } from '../../src/lib/constants'; + +describe('Starknet WalletInitializationBuilder', () => { + const coinConfig = coins.get('starknet'); + const chainId = SandboxTransferData.chainId; + + describe('Build deploy account transaction', () => { + it('should build DEPLOY_ACCOUNT and produce a transactionHash', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + const data = tx.starknetTransactionData; + + data.transactionType.should.equal(StarknetTransactionType.DEPLOY_ACCOUNT); + should.exist(data.transactionHash); + (data.transactionHash as string).should.startWith('0x'); + data.senderAddress.should.equal(Accounts.account1.address); + (data.classHash as string).should.equal(OZ_ETH_ACCOUNT_CLASS_HASH); + should.exist(data.constructorCalldata); + should.exist(data.contractAddressSalt); + data.calls.should.have.length(0); + }); + + it('should set signableHex from the Poseidon deploy hash', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + tx.signableHex.should.equal(tx.starknetTransactionData.transactionHash); + tx.id.should.equal(tx.starknetTransactionData.transactionHash); + }); + + it('should produce different hashes for different accounts', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + + const builder1 = factory.getWalletInitializationBuilder(); + builder1.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + const tx1 = (await builder1.build()) as Transaction; + + const builder2 = factory.getWalletInitializationBuilder(); + builder2.fromPublicKey(Accounts.account2.publicKey).nonce('0x0').chainId(chainId); + const tx2 = (await builder2.build()) as Transaction; + + tx1.signableHex.should.not.equal(tx2.signableHex); + }); + + it('should round-trip through toInternalHex and factory.from', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + const internalHex = tx.toInternalHex(); + + const factory2 = new TransactionBuilderFactory(coinConfig); + const builder2 = await factory2.from(internalHex); + const tx2 = (await builder2.build()) as Transaction; + + tx2.signableHex.should.equal(tx.signableHex); + tx2.starknetTransactionData.transactionType.should.equal(StarknetTransactionType.DEPLOY_ACCOUNT); + }); + + it('toBroadcastFormat should return DEPLOY_ACCOUNT RPC JSON', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(chainId); + + const tx = (await builder.build()) as Transaction; + const broadcast = tx.toBroadcastFormat(); + const parsed = JSON.parse(broadcast); + + parsed.type.should.equal('DEPLOY_ACCOUNT'); + parsed.version.should.equal('0x3'); + parsed.sender_address.should.equal(Accounts.account1.address); + parsed.class_hash.should.equal(OZ_ETH_ACCOUNT_CLASS_HASH); + parsed.constructor_calldata.should.be.Array().and.not.empty(); + parsed.contract_address_salt.should.startWith('0x'); + parsed.nonce.should.equal('0x0'); + parsed.resource_bounds.should.have.property('l2_gas'); + parsed.nonce_data_availability_mode.should.equal('L1'); + parsed.fee_data_availability_mode.should.equal('L1'); + parsed.should.not.have.property('calldata'); + parsed.should.not.have.property('account_deployment_data'); + }); + }); + + describe('Validation', () => { + it('should reject build without public key or deploy fields', async () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + builder.sender(Accounts.account1.address).nonce('0x0').chainId(chainId); + + await builder.build().should.be.rejectedWith(/public key|constructor calldata/i); + }); + + it('should reject mismatched address and public key', () => { + const factory = new TransactionBuilderFactory(coinConfig); + const builder = factory.getWalletInitializationBuilder(); + (() => + builder + .fromPublicKey(Accounts.account1.publicKey) + .sender(Accounts.account2.address, Accounts.account1.publicKey)).should.throw(/[Aa]ddress/); + }); + }); +}); + +describe('Starknet deploy account RPC wire format (live Sepolia)', () => { + it('should reach node validation (not param parse error) for unsigned deploy', async function (this: Mocha.Context) { + this.timeout(15000); + const factory = new TransactionBuilderFactory(coins.get('starknet')); + const builder = factory.getWalletInitializationBuilder(); + builder.fromPublicKey(Accounts.account1.publicKey).nonce('0x0').chainId(SandboxTransferData.chainId); + const tx = (await builder.build()) as Transaction; + const body = { + jsonrpc: '2.0', + method: 'starknet_addDeployAccountTransaction', + params: [JSON.parse(tx.toBroadcastFormat())], + id: 1, + }; + + const response = await fetch('https://api.cartridge.gg/x/starknet/sepolia', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + const json = (await response.json()) as { error?: { message?: string } }; + + should.exist(json.error); + const msg = json.error?.message || ''; + msg.should.not.match(/parsing params|EOF/i); + msg.should.match(/signature|Validate|nonce|fee|resource|Invalid params/i); + }); +}); diff --git a/modules/sdk-coin-ton/src/ton.ts b/modules/sdk-coin-ton/src/ton.ts index 608c7c8d6d..4abdee90e7 100644 --- a/modules/sdk-coin-ton/src/ton.ts +++ b/modules/sdk-coin-ton/src/ton.ts @@ -76,7 +76,7 @@ export class Ton extends BaseCoin { } public getFullName(): string { - return 'Ton'; + return 'Gram'; } /** @inheritDoc */ diff --git a/modules/sdk-coin-ton/src/tton.ts b/modules/sdk-coin-ton/src/tton.ts index dbe1b75e00..6d5619ebfa 100644 --- a/modules/sdk-coin-ton/src/tton.ts +++ b/modules/sdk-coin-ton/src/tton.ts @@ -34,6 +34,6 @@ export class Tton extends Ton { * Complete human-readable name of this coin */ public getFullName(): string { - return 'Testnet Ton'; + return 'Testnet Gram'; } } diff --git a/modules/sdk-coin-ton/test/unit/ton.ts b/modules/sdk-coin-ton/test/unit/ton.ts index 862514851f..41e9215ea6 100644 --- a/modules/sdk-coin-ton/test/unit/ton.ts +++ b/modules/sdk-coin-ton/test/unit/ton.ts @@ -60,12 +60,12 @@ describe('TON:', function () { ton.getChain().should.equal('ton'); ton.getFamily().should.equal('ton'); - ton.getFullName().should.equal('Ton'); + ton.getFullName().should.equal('Gram'); ton.getBaseFactor().should.equal(1e9); tton.getChain().should.equal('tton'); tton.getFamily().should.equal('ton'); - tton.getFullName().should.equal('Testnet Ton'); + tton.getFullName().should.equal('Testnet Gram'); tton.getBaseFactor().should.equal(1e9); }); diff --git a/modules/sdk-core/src/bitgo/defi/defiVault.ts b/modules/sdk-core/src/bitgo/defi/defiVault.ts index 150d1d44ad..63071f38f3 100644 --- a/modules/sdk-core/src/bitgo/defi/defiVault.ts +++ b/modules/sdk-core/src/bitgo/defi/defiVault.ts @@ -52,7 +52,8 @@ export class DefiVault implements IDefiVault { * * Internally issues two sendMany calls (approve + deposit) and returns the * operationId that links them. If the deposit sendMany fails after - * the approve succeeds, the approve is auto-cancelled (fail-fast). + * the approve succeeds, the error propagates — the server-side reconciler + * handles orphaned approvals. * * @param params.vaultId - DeFi-service vault identifier * @param params.amount - amount in base units of the underlying asset @@ -67,15 +68,16 @@ export class DefiVault implements IDefiVault { throw new Error('amount is required'); } - // Layer-1 pre-flight: reject if an active deposit already exists for this (wallet, vault) - const activeOps: DefiOperationListResult = await this.bitgo - .get(this.bitgo.microservicesUrl(this.operationsUrl())) - .query({ vaultId: params.vaultId, state: 'active' }) - .result(); - - if (activeOps.items && activeOps.items.length > 0) { - throw new ActiveOperationExistsError(activeOps.items[0].operationId); - } + // TODO(CGD-1709): Re-enable active operation pre-flight check once the + // defi-service operations endpoint is deployed and returning active state. + // const activeOps: DefiOperationListResult = await this.bitgo + // .get(this.bitgo.microservicesUrl(this.operationsUrl())) + // .query({ vaultId: params.vaultId, state: 'active' }) + // .result(); + // + // if (activeOps.items && activeOps.items.length > 0) { + // throw new ActiveOperationExistsError(activeOps.items[0].operationId); + // } // Step 1: Approve txRequest via sendMany const approveResult = await this.wallet.sendMany({ diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index e6c7d3dcb9..9feb9e6686 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -10,6 +10,8 @@ export interface WebauthnInfo { prfSalt: string; otpDeviceId: string; encryptedPrv: string; + /** Required by POST /key to validate the PRF salt; not needed on the PUT /key/:id update path. */ + enterpriseId?: string; } import type { WebauthnKeyEncryptionInfo } from '../wallet/iWallets'; diff --git a/modules/sdk-core/src/bitgo/utils/mpcUtils.ts b/modules/sdk-core/src/bitgo/utils/mpcUtils.ts index e328512199..6e8a610326 100644 --- a/modules/sdk-core/src/bitgo/utils/mpcUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/mpcUtils.ts @@ -138,11 +138,19 @@ export abstract class MpcUtils { ); } - if (['transferAccept', 'transferReject'].includes(params.intentType) && baseCoin.getFamily() === 'canton') { + if ( + ['transferAccept', 'transferReject', 'cosignDelegationAccept', 'allocationAllocate'].includes( + params.intentType + ) && + baseCoin.getFamily() === 'canton' + ) { assert(params.txRequestId, `'txRequestId' is required parameter for ${params.intentType} intent`); } - if (params.intentType === 'transferOfferWithdrawn' && baseCoin.getFamily() === 'canton') { + if ( + ['transferOfferWithdrawn', 'allocationAllocateWithdrawn'].includes(params.intentType) && + baseCoin.getFamily() === 'canton' + ) { assert(params.transferOfferId, `'transferOfferId' is required parameter for ${params.intentType} intent`); } @@ -181,6 +189,9 @@ export abstract class MpcUtils { 'transferAccept', 'transferReject', 'transferOfferWithdrawn', + 'cosignDelegationAccept', + 'allocationAllocate', + 'allocationAllocateWithdrawn', 'bridgeFunds', 'cantonCommand', 'defi-approve', diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts index 3a3f7a5099..b386a23871 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts @@ -145,6 +145,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, webauthnInfo: params.webauthnInfo, encryptionVersion: params.encryptionVersion, + enterprise: params.enterprise, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, @@ -187,6 +188,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionVersion, + enterprise, }: CreateEcdsaKeychainParams): Promise { if (!passphrase) { throw new Error('Please provide a wallet passphrase'); @@ -203,7 +205,8 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); } @@ -322,7 +325,8 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { const bitgoKeyShares = bitgoKeychain.keyShares; if (!bitgoKeyShares) { @@ -407,7 +411,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { ); const prv = JSON.stringify(recipientCombinedKey.signingMaterial); - const recipientKeychainParams = { + const recipientKeychainParams: AddKeychainOptions & { prv: string } = { source: recipient, keyType: 'tss' as KeyType, commonKeychain: bitgoKeychain.commonKeychain, @@ -418,22 +422,23 @@ export class EcdsaUtils extends BaseEcdsaUtils { encryptionVersion, }), originalPasscodeEncryptionCode, - webauthnDevices: - webauthnInfo && recipientIndex === ShareKeyPosition.USER - ? [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: prv, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ] - : undefined, }; + if (webauthnInfo && recipientIndex === ShareKeyPosition.USER) { + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + recipientKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: prv, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; + } + const keychains = this.baseCoin.keychains(); return recipientIndex === 1 ? await keychains.add(recipientKeychainParams) diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index 337d853157..03d16dd215 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -336,7 +336,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { params.originalPasscodeEncryptionCode, params.webauthnInfo, encryptionSession, - params.encryptionVersion + params.encryptionVersion, + params.enterprise ); const backupKeychainPromise = this.addBackupKeychain( bitgoCommonKeychain, @@ -380,7 +381,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { decrypt(ciphertext: string): Promise; destroy(): void; }, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -435,17 +437,18 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { }; if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) { - recipientKeychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: privateMaterialBase64, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + recipientKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: privateMaterialBase64, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } const keychains = this.baseCoin.keychains(); @@ -574,7 +577,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { decrypt(ciphertext: string): Promise; destroy(): void; }, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -585,7 +589,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionSession, - encryptionVersion + encryptionVersion, + enterprise ); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index a439e47dae..5cad5b6b82 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -135,6 +135,7 @@ export class EddsaUtils extends baseTSSUtils { webauthnInfo, encryptionSession, encryptionVersion, + enterprise, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -202,17 +203,18 @@ export class EddsaUtils extends baseTSSUtils { } } if (webauthnInfo && userKeychainParams.encryptedPrv) { - userKeychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: JSON.stringify(userSigningMaterial), - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + userKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: JSON.stringify(userSigningMaterial), + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } return await this.baseCoin.keychains().add(userKeychainParams); @@ -412,6 +414,7 @@ export class EddsaUtils extends baseTSSUtils { webauthnInfo: params.webauthnInfo, encryptionSession, encryptionVersion: params.encryptionVersion, + enterprise: params.enterprise, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index e746fbe282..70f29e81ab 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -195,7 +195,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { params.passphrase, params.originalPasscodeEncryptionCode, params.webauthnInfo, - params.encryptionVersion + params.encryptionVersion, + params.enterprise ); const backupKeychainPromise = this.addBackupKeychain( backupCommonKeychain, @@ -230,7 +231,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase?: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -279,17 +281,18 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { }; if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) { - keychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: privateMaterialBase64, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + keychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: privateMaterialBase64, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } const keychains = this.baseCoin.keychains(); @@ -303,7 +306,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -313,7 +317,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); } diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index 88476e42a2..a277c0c771 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -63,6 +63,19 @@ export interface GenerateSMCMpcWalletOptions extends GenerateBaseMpcWalletOption coldDerivationSeed?: string; } +export interface CreateKeychainCallbackParams { + source: 'user' | 'backup'; + coin: string; +} + +export interface CreateKeychainCallbackResult { + pub: string; + type: 'independent'; + source: 'user' | 'backup'; +} + +export type CreateKeychainCallback = (params: CreateKeychainCallbackParams) => Promise; + export interface GenerateWalletOptions { label?: string; passphrase?: string; @@ -95,6 +108,16 @@ export interface GenerateWalletOptions { /** Optional WebAuthn PRF-based encryption info. When provided, the user private key is additionally encrypted with the PRF-derived passphrase so the server can store a WebAuthn-protected copy. */ webauthnInfo?: WebauthnKeyEncryptionInfo; encryptionVersion?: EncryptionVersion; + /** Delegates user/backup key creation to an external signer (onchain multisig only). */ + createKeychainCallback?: CreateKeychainCallback; +} + +export interface GenerateWalletWithExternalSignerOptions + extends Omit { + label: string; + createKeychainCallback: CreateKeychainCallback; + /** Optional user-key signatures over backup/bitgo pubs. Omit when the external signer cannot produce them (equivalent to a cold wallet). */ + keySignatures?: { backup: string; bitgo: string }; } export const GenerateLightningWalletOptionsCodec = t.intersection( @@ -284,6 +307,7 @@ export interface IWallets { generateWallet( params?: GenerateWalletOptions ): Promise; + generateWalletWithExternalSigner(params: GenerateWalletWithExternalSignerOptions): Promise; listShares(params?: Record): Promise; getShare(params?: { walletShareId?: string }): Promise; updateShare(params?: UpdateShareOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 46c4e9a0b7..4d43c53b77 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -3973,6 +3973,20 @@ export class Wallet implements IWallet { } // Check if we build with intent if (this._wallet.multisigType === 'tss') { + // Populate recipients from enableTokens so verifyTransaction can access tokenName. + // enableTokens is kept (not deleted) since the server needs it to build the transaction. + buildParams.recipients = params.enableTokens.map((token) => { + const address = + token.address || this._wallet.coinSpecific?.baseAddress || this._wallet.coinSpecific?.rootAddress; + if (!address) { + throw new Error('Wallet does not have base address, must specify with token param'); + } + return { + tokenName: token.name, + address, + amount: '0', + }; + }); return [await this.prebuildTransaction(buildParams)]; } else { // Rewrite tokens into recipients for buildTransaction @@ -4294,6 +4308,32 @@ export class Wallet implements IWallet { ); break; } + case 'cosignDelegationAccept': { + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'cosignDelegationAccept', + txRequestId: params.txRequestId, + sequenceId: params.txRequestId, + }, + apiVersion, + params.preview + ); + break; + } + case 'allocationAllocate': { + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'allocationAllocate', + txRequestId: params.txRequestId, + sequenceId: params.txRequestId, + }, + apiVersion, + params.preview + ); + break; + } case 'transferReject': { txRequest = await this.tssUtils!.prebuildTxWithIntent( { @@ -4320,6 +4360,19 @@ export class Wallet implements IWallet { ); break; } + case 'allocationAllocateWithdrawn': { + txRequest = await this.tssUtils!.prebuildTxWithIntent( + { + reqId, + intentType: 'allocationAllocateWithdrawn', + transferOfferId: params.transferOfferId, + sequenceId: params.transferOfferId, + }, + apiVersion, + params.preview + ); + break; + } case 'cantonCommand': { if (!params.cantonCommandParams) { throw new Error('cantonCommandParams is required for cantonCommand intent'); diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index 377fdfe5f6..b6febbc929 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -31,6 +31,7 @@ import { GenerateMpcWalletOptions, GenerateSMCMpcWalletOptions, GenerateWalletOptions, + GenerateWalletWithExternalSignerOptions, GetWalletByAddressOptions, GetWalletOptions, GoAccountWalletWithUserKeychain, @@ -360,6 +361,10 @@ export class Wallets implements IWallets { throw new Error('missing required string parameter label'); } + if (params.createKeychainCallback) { + return this.generateWalletWithExternalSigner(params as GenerateWalletWithExternalSignerOptions); + } + const { type = 'hot', label, passphrase, enterprise, isDistributedCustody, evmKeyRingReferenceWalletId } = params; const isTss = params.multisigType === 'tss' && this.baseCoin.supportsTss(); const canEncrypt = !!passphrase && typeof passphrase === 'string'; @@ -601,18 +606,19 @@ export class Wallets implements IWallets { // If WebAuthn info is provided, store an additional copy of the private key encrypted // with the PRF-derived passphrase so the authenticator can later decrypt it. + // Send it as `webauthnInfo` (single object, with enterpriseId) so the atomic + // POST /key persists the passkey — the backend ignores the deprecated `webauthnDevices`. if (params.webauthnInfo && userKeychain.prv) { - userKeychainParams.webauthnDevices = [ - { - otpDeviceId: params.webauthnInfo.otpDeviceId, - prfSalt: params.webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - password: params.webauthnInfo.passphrase, - input: userKeychain.prv, - encryptionVersion: params.encryptionVersion, - }), - }, - ]; + userKeychainParams.webauthnInfo = { + otpDeviceId: params.webauthnInfo.otpDeviceId, + prfSalt: params.webauthnInfo.prfSalt, + enterpriseId: params.enterprise, + encryptedPrv: await this.bitgo.encryptAsync({ + password: params.webauthnInfo.passphrase, + input: userKeychain.prv, + encryptionVersion: params.encryptionVersion, + }), + }; } } @@ -718,6 +724,181 @@ export class Wallets implements IWallets { } } + /** + * Generate an onchain multisig wallet using an external signer for user and backup key creation. + * 1. Calls createKeychainCallback for user and backup keys + * 2. Uploads keychains via keychains().add() + * 3. Creates the BitGo key on the service + * 4. Creates the wallet on BitGo with the 3 public keys + * @param params + */ + async generateWalletWithExternalSigner( + params: GenerateWalletWithExternalSignerOptions + ): Promise { + if (!_.isFunction(params.createKeychainCallback)) { + throw new Error('missing required function parameter createKeychainCallback'); + } + + const multisigType = params.multisigType ?? this.baseCoin.getDefaultMultisigType(); + if (multisigType !== 'onchain') { + throw new Error('external signer wallet generation is only supported for onchain multisig wallets'); + } + + // these belong to the passphrase-based path and are incompatible with createKeychainCallback + const passphrasePathParams = ['passphrase', 'userKey', 'backupXpub', 'backupXpubProvider'] as const; + for (const key of passphrasePathParams) { + if (!_.isUndefined(params[key])) { + throw new Error(`createKeychainCallback cannot be used with ${key}`); + } + } + + const { label, createKeychainCallback, type = 'hot', enterprise, isDistributedCustody } = params; + + if (type === 'custodial') { + throw new Error('external signer wallet generation is not supported for custodial onchain wallets'); + } + + if (!_.isUndefined(params.webauthnInfo)) { + throw new Error('webauthnInfo is not supported for external signer wallet generation'); + } + + if (!_.isUndefined(params.passcodeEncryptionCode)) { + throw new Error('passcodeEncryptionCode is not supported for external signer wallet generation'); + } + + if (isDistributedCustody) { + if (!enterprise) { + throw new Error('must provide enterprise when creating distributed custody wallet'); + } + if (type !== 'cold') { + throw new Error('distributed custody wallets must be type: cold'); + } + } + + if (params.gasPrice && params.eip1559) { + throw new Error('can not use both eip1559 and gasPrice values'); + } + + const walletParams: SupplementGenerateWalletOptions = { + label, + m: 2, + n: 3, + keys: [], + type, + }; + + if (!_.isUndefined(enterprise)) { + if (!_.isString(enterprise)) { + throw new Error('invalid enterprise argument, expecting string'); + } + walletParams.enterprise = enterprise; + } + + if (!_.isUndefined(params.disableTransactionNotifications)) { + if (!_.isBoolean(params.disableTransactionNotifications)) { + throw new Error('invalid disableTransactionNotifications argument, expecting boolean'); + } + walletParams.disableTransactionNotifications = params.disableTransactionNotifications; + } + + if (!_.isUndefined(params.gasPrice)) { + const gasPriceBN = new BigNumber(params.gasPrice); + if (gasPriceBN.isNaN()) { + throw new Error('invalid gas price argument, expecting number or number as string'); + } + walletParams.gasPrice = gasPriceBN.toString(); + } + + if (!_.isUndefined(params.eip1559) && !_.isEmpty(params.eip1559)) { + const maxFeePerGasBN = new BigNumber(params.eip1559.maxFeePerGas); + if (maxFeePerGasBN.isNaN()) { + throw new Error('invalid max fee argument, expecting number or number as string'); + } + const maxPriorityFeePerGasBN = new BigNumber(params.eip1559.maxPriorityFeePerGas); + if (maxPriorityFeePerGasBN.isNaN()) { + throw new Error('invalid priority fee argument, expecting number or number as string'); + } + walletParams.eip1559 = { + maxFeePerGas: maxFeePerGasBN.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGasBN.toString(), + }; + } + + if (!_.isUndefined(params.walletVersion)) { + if (!_.isNumber(params.walletVersion)) { + throw new Error('invalid walletVersion provided, expecting number'); + } + walletParams.walletVersion = params.walletVersion; + } + + const reqId = new RequestTracer(); + const coin = this.baseCoin.getChain(); + + const createAndUploadKeychain = async (source: 'user' | 'backup'): Promise => { + try { + const keychainFromCallback = await createKeychainCallback({ source, coin }); + if (keychainFromCallback.source !== source) { + throw new Error(`createKeychainCallback returned source ${keychainFromCallback.source}, expected ${source}`); + } + if (keychainFromCallback.type !== 'independent') { + throw new Error( + `createKeychainCallback returned invalid type ${keychainFromCallback.type}, expected 'independent' for onchain multisig` + ); + } + if (!this.baseCoin.isValidPub(keychainFromCallback.pub)) { + throw new Error(`createKeychainCallback returned invalid pub for ${source} key on ${coin}`); + } + return this.baseCoin.keychains().add({ + pub: keychainFromCallback.pub, + keyType: keychainFromCallback.type, + source: keychainFromCallback.source, + reqId, + }); + } catch (error) { + throw new Error( + `Failed to create ${source} keychain: ${error instanceof Error ? error.message : String(error)}` + ); + } + }; + + const { userKeychain, backupKeychain, bitgoKeychain }: KeychainsTriplet = await promiseProps({ + userKeychain: createAndUploadKeychain('user'), + backupKeychain: createAndUploadKeychain('backup'), + bitgoKeychain: this.baseCoin + .keychains() + .createBitGo({ enterprise, reqId, isDistributedCustody: params.isDistributedCustody }), + }); + + walletParams.keys = [userKeychain.id, backupKeychain.id, bitgoKeychain.id]; + + if (params.keySignatures) { + walletParams.keySignatures = params.keySignatures; + } + + const keychains = { + userKeychain, + backupKeychain, + bitgoKeychain, + }; + + if (_.includes(['xrp', 'xlm', 'cspr'], this.baseCoin.getFamily()) && !_.isUndefined(params.rootPrivateKey)) { + walletParams.rootPrivateKey = params.rootPrivateKey; + } + + const finalWalletParams = await this.baseCoin.supplementGenerateWallet(walletParams, keychains); + + this.bitgo.setRequestTracer(reqId); + const newWallet = await this.bitgo.post(this.baseCoin.url('/wallet/add')).send(finalWalletParams).result(); + + return { + wallet: new Wallet(this.bitgo, this.baseCoin, newWallet), + userKeychain, + backupKeychain, + bitgoKeychain, + responseType: 'WalletWithKeychains', + }; + } + /** * List the user's wallet shares * @param params diff --git a/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts b/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts index c4c810d06b..6a4377c920 100644 --- a/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts +++ b/modules/sdk-core/test/unit/bitgo/defi/defiVault.ts @@ -54,10 +54,6 @@ describe('DefiVault', function () { it('should call sendMany for approve and deposit on happy path', async function () { const operationId = 'op-uuid-123'; - // Pre-flight: no active operations - const preflightReq = mockRequest({ items: [] }); - mockBitGo.get.onFirstCall().returns(preflightReq); - // Mock sendMany for approve and deposit const sendManyStub = sinon.stub(wallet, 'sendMany'); sendManyStub.onFirstCall().resolves({ @@ -82,9 +78,6 @@ describe('DefiVault', function () { result.txRequestIds.approve.should.equal('txreq-approve-1'); result.txRequestIds.deposit.should.equal('txreq-deposit-1'); - // Verify pre-flight was called with correct query - preflightReq.query.calledWith({ vaultId: 'vlt-galaxy-usdc', state: 'active' }).should.be.true(); - // Verify sendMany was called with correct params for approve sendManyStub.calledTwice.should.be.true(); const approveArgs: any = sendManyStub.firstCall.args[0]; @@ -98,7 +91,8 @@ describe('DefiVault', function () { depositArgs.defiParams.operationId.should.equal(operationId); }); - it('should reject when an active operation already exists', async function () { + // TODO(CGD-1709): Re-enable when active operation pre-flight check is restored + xit('should reject when an active operation already exists', async function () { const preflightReq = mockRequest({ items: [{ operationId: 'existing-op-id', state: 'APPROVE_TX_REQUESTED' }], }); @@ -117,10 +111,6 @@ describe('DefiVault', function () { it('should propagate deposit sendMany failure without cleanup', async function () { const operationId = 'op-uuid-456'; - // Pre-flight: no active operations - const preflightReq = mockRequest({ items: [] }); - mockBitGo.get.returns(preflightReq); - // Mock sendMany: approve succeeds, deposit fails const sendManyStub = sinon.stub(wallet, 'sendMany'); sendManyStub.onFirstCall().resolves({ @@ -154,9 +144,6 @@ describe('DefiVault', function () { it('should pass clientIdempotencyKey and walletPassphrase when provided', async function () { const operationId = 'op-uuid-789'; - const preflightReq = mockRequest({ items: [] }); - mockBitGo.get.returns(preflightReq); - const sendManyStub = sinon.stub(wallet, 'sendMany'); sendManyStub.onFirstCall().resolves({ txRequest: { diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts new file mode 100644 index 0000000000..428a197918 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsExternalSigner.ts @@ -0,0 +1,328 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import 'should'; + +import { Wallets } from '../../../../src/bitgo/wallet/wallets'; +import { CreateKeychainCallback } from '../../../../src/bitgo/wallet/iWallets'; +import { Wallet } from '../../../../src/bitgo/wallet/wallet'; + +describe('Wallets - external signer onchain wallet generation', function () { + let wallets: Wallets; + let mockBitGo: any; + let mockBaseCoin: any; + let mockKeychains: any; + let createKeychainCallback: sinon.SinonStub, ReturnType>; + let sendStub: sinon.SinonStub; + + const userPub = + 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; + const backupPub = + 'xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa'; + const bitgoPub = + 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm'; + + beforeEach(function () { + createKeychainCallback = sinon.stub(); + createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ + pub: userPub, + type: 'independent', + source: 'user', + }); + createKeychainCallback.withArgs({ source: 'backup', coin: 'tbtc' }).resolves({ + pub: backupPub, + type: 'independent', + source: 'backup', + }); + + mockKeychains = { + add: sinon.stub().callsFake(async (params: { pub: string; source: string }) => ({ + id: `${params.source}-key-id`, + pub: params.pub, + source: params.source, + })), + createBitGo: sinon.stub().resolves({ id: 'bitgo-key-id', pub: bitgoPub }), + }; + + const mockWalletData = { id: 'wallet-id', keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'] }; + + sendStub = sinon.stub().returns({ + result: sinon.stub().resolves(mockWalletData), + }); + mockBitGo = { + post: sinon.stub().returns({ send: sendStub }), + setRequestTracer: sinon.stub(), + }; + + mockBaseCoin = { + isEVM: sinon.stub().returns(false), + supportsTss: sinon.stub().returns(true), + getFamily: sinon.stub().returns('btc'), + getChain: sinon.stub().returns('tbtc'), + getDefaultMultisigType: sinon.stub().returns('onchain'), + keychains: sinon.stub().returns(mockKeychains), + url: sinon.stub().returns('/api/v2/tbtc/wallet/add'), + getConfig: sinon.stub().returns({ features: [] }), + supplementGenerateWallet: sinon.stub().callsFake((walletParams: unknown) => Promise.resolve(walletParams)), + isValidPub: sinon.stub().returns(true), + }; + + wallets = new Wallets(mockBitGo, mockBaseCoin); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('generateWalletWithExternalSigner', function () { + it('should create user and backup keys via callback and create wallet', async function () { + const result = await wallets.generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + enterprise: 'enterprise-id', + createKeychainCallback, + }); + + assert.strictEqual(createKeychainCallback.callCount, 2); + assert.strictEqual(mockKeychains.add.callCount, 2); + assert.strictEqual(mockKeychains.createBitGo.calledOnce, true); + assert.strictEqual(mockBitGo.post.calledOnce, true); + + const addUserParams = mockKeychains.add.getCall(0).args[0]; + addUserParams.should.have.property('pub', userPub); + addUserParams.should.have.property('keyType', 'independent'); + addUserParams.should.have.property('source', 'user'); + + const walletBody = sendStub.firstCall.args[0]; + walletBody.keys.should.deepEqual(['user-key-id', 'backup-key-id', 'bitgo-key-id']); + walletBody.label.should.equal('External Signer Wallet'); + walletBody.enterprise.should.equal('enterprise-id'); + + result.responseType.should.equal('WalletWithKeychains'); + assert.strictEqual(result.userKeychain.pub, userPub); + assert.strictEqual(result.backupKeychain.pub, backupPub); + assert.strictEqual(result.bitgoKeychain.pub, bitgoPub); + }); + + it('should reject when callback source does not match requested source', async function () { + createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ + pub: userPub, + type: 'independent', + source: 'backup', + }); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith( + 'Failed to create user keychain: createKeychainCallback returned source backup, expected user' + ); + }); + + it('should reject invalid type from callback', async function () { + const badTypeCallback = sinon.stub(); + badTypeCallback.withArgs({ source: 'user', coin: 'tbtc' }).resolves({ + pub: userPub, + type: 'tss', + source: 'user', + }); + badTypeCallback.withArgs({ source: 'backup', coin: 'tbtc' }).resolves({ + pub: backupPub, + type: 'independent', + source: 'backup', + }); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback: badTypeCallback, + }) + .should.be.rejectedWith( + "Failed to create user keychain: createKeychainCallback returned invalid type tss, expected 'independent' for onchain multisig" + ); + }); + + it('should reject TSS multisig type', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'TSS Wallet', + multisigType: 'tss', + createKeychainCallback, + }) + .should.be.rejectedWith('external signer wallet generation is only supported for onchain multisig wallets'); + }); + + it('should reject custodial wallet type', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'Custodial Wallet', + type: 'custodial', + createKeychainCallback, + }) + .should.be.rejectedWith('external signer wallet generation is not supported for custodial onchain wallets'); + }); + + it('should reject passcodeEncryptionCode', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'Wallet', + passcodeEncryptionCode: 'some-code', + createKeychainCallback, + }) + .should.be.rejectedWith('passcodeEncryptionCode is not supported for external signer wallet generation'); + }); + + it('should reject webauthnInfo', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'Wallet', + webauthnInfo: { otpDeviceId: 'dev-id', prfSalt: 'salt', passphrase: 'pass' }, + createKeychainCallback, + }) + .should.be.rejectedWith('webauthnInfo is not supported for external signer wallet generation'); + }); + + it('should reject isDistributedCustody without enterprise', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'DC Wallet', + type: 'cold', + isDistributedCustody: true, + createKeychainCallback, + }) + .should.be.rejectedWith('must provide enterprise when creating distributed custody wallet'); + }); + + it('should reject isDistributedCustody with non-cold type', async function () { + await wallets + .generateWalletWithExternalSigner({ + label: 'DC Wallet', + type: 'hot', + isDistributedCustody: true, + enterprise: 'enterprise-id', + createKeychainCallback, + }) + .should.be.rejectedWith('distributed custody wallets must be type: cold'); + }); + + it('should reject when callback returns invalid pub for coin', async function () { + // only invalidate user pub so the rejection source is deterministic + mockBaseCoin.isValidPub.callsFake((pub: string) => pub !== userPub); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith( + 'Failed to create user keychain: createKeychainCallback returned invalid pub for user key on tbtc' + ); + }); + + it('should wrap error when callback throws', async function () { + createKeychainCallback.withArgs({ source: 'user', coin: 'tbtc' }).rejects(new Error('HSM unreachable')); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('Failed to create user keychain: HSM unreachable'); + }); + + it('should reject with backup keychain error when backup callback throws', async function () { + createKeychainCallback.withArgs({ source: 'backup', coin: 'tbtc' }).rejects(new Error('HSM unreachable')); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('Failed to create backup keychain: HSM unreachable'); + + assert.strictEqual(mockBitGo.post.callCount, 0); + }); + + it('should wrap non-Error thrown by callback', async function () { + createKeychainCallback + .withArgs({ source: 'user', coin: 'tbtc' }) + .returns(Promise.reject('plain string rejection')); + + await wallets + .generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + createKeychainCallback, + }) + .should.be.rejectedWith('Failed to create user keychain: plain string rejection'); + }); + + it('should forward keySignatures to wallet params when provided', async function () { + const keySignatures = { + backup: 'deadbeef01', + bitgo: 'deadbeef02', + }; + + await wallets.generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + enterprise: 'enterprise-id', + createKeychainCallback, + keySignatures, + }); + + const walletBody = sendStub.firstCall.args[0]; + walletBody.should.have.property('keySignatures'); + walletBody.keySignatures.should.deepEqual(keySignatures); + }); + + it('should not include keySignatures in wallet params when not provided', async function () { + await wallets.generateWalletWithExternalSigner({ + label: 'External Signer Wallet', + enterprise: 'enterprise-id', + createKeychainCallback, + }); + + const walletBody = sendStub.firstCall.args[0]; + walletBody.should.not.have.property('keySignatures'); + }); + }); + + describe('generateWallet with createKeychainCallback', function () { + it('should delegate to generateWalletWithExternalSigner', async function () { + const generateWalletWithExternalSignerStub = sinon.stub(wallets, 'generateWalletWithExternalSigner').resolves({ + responseType: 'WalletWithKeychains', + wallet: sinon.createStubInstance(Wallet), + userKeychain: { id: 'user-key-id', pub: userPub, type: 'independent' as const }, + backupKeychain: { id: 'backup-key-id', pub: backupPub, type: 'independent' as const }, + bitgoKeychain: { id: 'bitgo-key-id', pub: bitgoPub, type: 'independent' as const }, + }); + + await wallets.generateWallet({ + label: 'Delegated Wallet', + createKeychainCallback, + }); + + assert.strictEqual(generateWalletWithExternalSignerStub.calledOnce, true); + generateWalletWithExternalSignerStub.firstCall.args[0].label.should.equal('Delegated Wallet'); + }); + + it('should reject when createKeychainCallback is combined with passphrase', async function () { + await wallets + .generateWallet({ + label: 'Invalid Wallet', + passphrase: 'secret', + createKeychainCallback, + }) + .should.be.rejectedWith('createKeychainCallback cannot be used with passphrase'); + }); + + it('should reject when createKeychainCallback is combined with userKey', async function () { + await wallets + .generateWallet({ + label: 'Invalid Wallet', + userKey: 'xpub...', + createKeychainCallback, + }) + .should.be.rejectedWith('createKeychainCallback cannot be used with userKey'); + }); + }); +}); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts b/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts index 3167ee9e73..785a736849 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/walletsWebauthn.ts @@ -65,7 +65,7 @@ describe('Wallets - WebAuthn wallet creation', function () { }); describe('generateWallet with webauthnInfo', function () { - it('should add webauthnDevices to keychain params when webauthnInfo is provided', async function () { + it('should add webauthnInfo (with enterpriseId) to keychain params when webauthnInfo is provided', async function () { const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', @@ -75,16 +75,18 @@ describe('Wallets - WebAuthn wallet creation', function () { await wallets.generateWallet({ label: 'Test Wallet', passphrase: 'wallet-passphrase', + enterprise: 'enterprise-123', webauthnInfo, }); assert.strictEqual(mockKeychains.add.calledOnce, true); const addParams = mockKeychains.add.firstCall.args[0]; - addParams.should.have.property('webauthnDevices'); - addParams.webauthnDevices.should.have.length(1); - addParams.webauthnDevices[0].should.have.property('otpDeviceId', webauthnInfo.otpDeviceId); - addParams.webauthnDevices[0].should.have.property('prfSalt', webauthnInfo.prfSalt); - addParams.webauthnDevices[0].should.have.property('encryptedPrv'); + addParams.should.not.have.property('webauthnDevices'); + addParams.should.have.property('webauthnInfo'); + addParams.webauthnInfo.should.have.property('otpDeviceId', webauthnInfo.otpDeviceId); + addParams.webauthnInfo.should.have.property('prfSalt', webauthnInfo.prfSalt); + addParams.webauthnInfo.should.have.property('enterpriseId', 'enterprise-123'); + addParams.webauthnInfo.should.have.property('encryptedPrv'); }); it('should encrypt user private key with the webauthn passphrase', async function () { @@ -102,7 +104,7 @@ describe('Wallets - WebAuthn wallet creation', function () { const addParams = mockKeychains.add.firstCall.args[0]; const expectedEncryptedPrv = `encrypted:${webauthnPassphrase}:${userPrv}`; - addParams.webauthnDevices[0].should.have.property('encryptedPrv', expectedEncryptedPrv); + addParams.webauthnInfo.should.have.property('encryptedPrv', expectedEncryptedPrv); }); it('should also encrypt user private key with wallet passphrase when webauthnInfo is provided', async function () { @@ -143,7 +145,7 @@ describe('Wallets - WebAuthn wallet creation', function () { passwordsUsed.should.containEql(webauthnPassphrase); }); - it('should not add webauthnDevices when webauthnInfo is not provided', async function () { + it('should not add webauthnInfo when webauthnInfo is not provided', async function () { await wallets.generateWallet({ label: 'Test Wallet', passphrase: 'wallet-passphrase', @@ -151,11 +153,12 @@ describe('Wallets - WebAuthn wallet creation', function () { assert.strictEqual(mockKeychains.add.calledOnce, true); const addParams = mockKeychains.add.firstCall.args[0]; + addParams.should.not.have.property('webauthnInfo'); addParams.should.not.have.property('webauthnDevices'); }); - it('should not add webauthnDevices when userKey is explicitly provided (no prv available)', async function () { - // When a user-provided public key is used, there is no private key to encrypt, so webauthnDevices is skipped + it('should not add webauthnInfo when userKey is explicitly provided (no prv available)', async function () { + // When a user-provided public key is used, there is no private key to encrypt, so webauthnInfo is skipped await wallets.generateWallet({ label: 'Test Wallet', userKey: userPub, @@ -167,10 +170,11 @@ describe('Wallets - WebAuthn wallet creation', function () { }, }); - // add is called for both user keychain (pub-only) and backup keychain - neither should have webauthnDevices + // add is called for both user keychain (pub-only) and backup keychain - neither should have webauthnInfo const allAddCalls = mockKeychains.add.getCalls(); assert.ok(allAddCalls.length > 0, 'expected keychains().add to be called at least once'); for (const call of allAddCalls) { + call.args[0].should.not.have.property('webauthnInfo'); call.args[0].should.not.have.property('webauthnDevices'); } }); diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index ffd1f716a1..86896a139b 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -76,6 +76,7 @@ import { jettonTokens } from './coins/jettonTokens'; import { erc7984Tokens } from './coins/erc7984Tokens'; import { polyxTokens } from './coins/polyxTokens'; import { cantonTokens } from './coins/cantonTokens'; +import { hoodethTokens } from './coins/hoodethTokens'; import { flrp } from './flrp'; import { hypeEvm } from './hypeevm'; import { kaspa } from './kaspa'; @@ -171,6 +172,7 @@ export const allCoinsAndTokens = [ ...erc7984Tokens, ...polyxTokens, ...cantonTokens, + ...hoodethTokens, avaxp( '5436386e-9e4d-4d82-92df-59d9720d1738', 'avaxp', @@ -1416,7 +1418,7 @@ export const allCoinsAndTokens = [ account( 'b5ba2fc6-706b-433f-9bcf-4ea4aaa09281', 'ton', - 'Ton', + 'Gram', Networks.main.ton, 9, UnderlyingAsset.TON, @@ -1427,7 +1429,7 @@ export const allCoinsAndTokens = [ account( '8244f85f-943c-4520-8e68-9e7f4361a13f', 'tton', - 'Testnet Ton', + 'Testnet Gram', Networks.test.ton, 9, UnderlyingAsset.TON, @@ -2678,11 +2680,14 @@ export const allCoinsAndTokens = [ BaseUnit.ETH, [ ...EVM_FEATURES, + CoinFeature.EVM_COMPATIBLE_WP, CoinFeature.SHARED_EVM_SIGNING, CoinFeature.SHARED_EVM_SDK, CoinFeature.EVM_COMPATIBLE_IMS, CoinFeature.EVM_COMPATIBLE_UI, CoinFeature.SUPPORTS_ERC20, + CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, ] ), account( @@ -2695,11 +2700,14 @@ export const allCoinsAndTokens = [ BaseUnit.ETH, [ ...EVM_FEATURES, + CoinFeature.EVM_COMPATIBLE_WP, CoinFeature.SHARED_EVM_SIGNING, CoinFeature.SHARED_EVM_SDK, CoinFeature.EVM_COMPATIBLE_IMS, CoinFeature.EVM_COMPATIBLE_UI, CoinFeature.SUPPORTS_ERC20, + CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, ] ), @@ -3121,24 +3129,6 @@ export const allCoinsAndTokens = [ '', 'THoodETH' ), - erc20Token( - '3493d608-fd3e-45dc-926d-783d54a8fe4d', - 'thoodeth:amzn', - 'Amazon', - 18, - '0x5884ad2f920c162cfbbacc88c9c51aa75ec09e02', - UnderlyingAsset['thoodeth:amzn'], - Networks.test.hoodeth - ), - erc20Token( - '8ede8dbd-1fa6-4669-be6d-6b19b3c98766', - 'thoodeth:tsla', - 'Tesla', - 18, - '0xc9f9c86933092bbbfff3ccb4b105a4a94bf3bd4e', - UnderlyingAsset['thoodeth:tsla'], - Networks.test.hoodeth - ), account( '1b17bbf4-02fc-492d-9071-6d7f47395f7a', 'hoodeth', @@ -3162,255 +3152,6 @@ export const allCoinsAndTokens = [ '', 'HoodETH' ), - erc20Token( - '71bfcfbb-0e1d-4712-9836-8e7c481b9d87', - 'hoodeth:tsla', - 'Tesla', - 18, - '0x322f0929c4625ed5bad873c95208d54e1c003b2d', - UnderlyingAsset['hoodeth:tsla'], - Networks.main.hoodeth - ), - erc20Token( - 'ca3ac76e-e88f-4f39-8571-006cd9b97389', - 'hoodeth:usdg', - 'USDG', - 18, - '0x5fc5360d0400a0fd4f2af552add042d716f1d168', - UnderlyingAsset['hoodeth:usdg'], - Networks.main.hoodeth - ), - // Robinhood Chain tokens - CECHO-1183 (gated for BitGo Singapore Trust) - erc20Token( - 'eada9a10-3d17-465f-978b-5a183e73f4c6', - 'hoodeth:nvda', - 'NVIDIA Corporation', - 18, - '0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec', - UnderlyingAsset['hoodeth:nvda'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '13ebc2ae-5bee-4433-b5f1-94d823c0db20', - 'hoodeth:mu', - 'Micron Technology', - 18, - '0xff080c8ce2e5feadaca0da81314ae59d232d4afd', - UnderlyingAsset['hoodeth:mu'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'b3a25405-760f-4578-bbe8-14ddfbe3aacd', - 'hoodeth:sndk', - 'SanDisk', - 18, - '0xb90a19ff0af67f7779aff50a882a9cff42446400', - UnderlyingAsset['hoodeth:sndk'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '2597a273-8103-4e42-89cd-da241c231bc0', - 'hoodeth:amd', - 'Advanced Micro Devices, Inc.', - 18, - '0x86923f96303d656e4aa86d9d42d1e57ad2023fdc', - UnderlyingAsset['hoodeth:amd'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'e3613375-49ff-4279-9c57-e5fb9728a931', - 'hoodeth:spy', - 'SPDR S&P 500 ETF Trust', - 18, - '0x117cc2133c37b721f49de2a7a74833232b3b4c0c', - UnderlyingAsset['hoodeth:spy'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '49c855b1-0d85-4f79-9756-7ecf28440225', - 'hoodeth:msft', - 'Microsoft Corporation', - 18, - '0xe93237c50d904957cf27e7b1133b510c669c2e74', - UnderlyingAsset['hoodeth:msft'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'f4e2c9a2-130f-4080-ba53-d43d5494ee2b', - 'hoodeth:pltr', - 'Palantir Technologies Inc.', - 18, - '0x894e1ec2d74ffe5aef8dc8a9e84686accb964f2a', - UnderlyingAsset['hoodeth:pltr'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '8aa38d75-8f24-4fe4-8642-072e3e0529bd', - 'hoodeth:intc', - 'Intel Corporation', - 18, - '0xc72b96e0e48ecd4dc75e1e45396e26300bc39681', - UnderlyingAsset['hoodeth:intc'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '92f0c908-efa9-490a-a344-15511d54ce57', - 'hoodeth:qqq', - 'Invesco QQQ Trust', - 18, - '0xd5f3879160bc7c32ebb4dc785f8a4f505888de68', - UnderlyingAsset['hoodeth:qqq'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '326f35b5-d998-4851-81b3-fcb5c0ccf0ec', - 'hoodeth:slv', - 'iShares Silver Trust', - 18, - '0x411efb0e7f985935daec3d4c3ebaea0d0ad7d89f', - UnderlyingAsset['hoodeth:slv'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'c7491c49-a509-4791-aa18-2ab12130c518', - 'hoodeth:crcl', - 'Circle Internet Group Inc', - 18, - '0xdf0992e440dd0be65bd8439b609d6d4366bf1cb5', - UnderlyingAsset['hoodeth:crcl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'df3845bb-bab1-49d4-a040-c81a768d244a', - 'hoodeth:meta', - 'Meta Platforms, Inc.', - 18, - '0xc0d6457c16cc70d6790dd43521c899c87ce02f35', - UnderlyingAsset['hoodeth:meta'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'da8769ad-98d5-4349-bea9-8fee3be83c9a', - 'hoodeth:aapl', - 'Apple Inc.', - 18, - '0xaf3d76f1834a1d425780943c99ea8a608f8a93f9', - UnderlyingAsset['hoodeth:aapl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '31baa733-4e2b-4fc6-82ea-2653cbd083ff', - 'hoodeth:googl', - 'Alphabet Inc.', - 18, - '0x2e0847e8910a9732eb3fb1bb4b70a580adad4fe3', - UnderlyingAsset['hoodeth:googl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'f6f0a6f8-fea8-4306-baaf-c88a96b071de', - 'hoodeth:uso', - 'United States Oil Fund, LP', - 18, - '0xa30fa36db767ad9ed3f7a60fc79526fb4d56d344', - UnderlyingAsset['hoodeth:uso'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '6852e072-48ec-41e7-b0ad-c8594c560f5c', - 'hoodeth:amzn', - 'Amazon.com, Inc.', - 18, - '0x12f190a9f9d7d37a250758b26824b97ce941bf54', - UnderlyingAsset['hoodeth:amzn'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '9c12894d-62c6-4e9c-bea7-9d909a23c496', - 'hoodeth:crwv', - 'CoreWeave', - 18, - '0x5f10a1c971b69e47e059e1dc91901b59b3fb49c3', - UnderlyingAsset['hoodeth:crwv'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '0519b754-a119-4431-9d00-490522e2a4d1', - 'hoodeth:orcl', - 'Oracle Corporation', - 18, - '0xb0992820e760d836549ba69bc7598b4af75dee03', - UnderlyingAsset['hoodeth:orcl'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'be7d01f7-12e9-4e19-91be-56d894c9fa9c', - 'hoodeth:sgov', - 'iShares 0-3 Month Treasury Bond ETF', - 18, - '0x92fd66527192e3e61d4ddd13322aa222de86f9b5', - UnderlyingAsset['hoodeth:sgov'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'ffb9fa51-e51b-40f0-a2c9-3f735bab7d7c', - 'hoodeth:be', - 'Bloom Energy Corporation', - 18, - '0x822cc93ffd030293e9842c30bbd678f530701867', - UnderlyingAsset['hoodeth:be'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '7d0446f2-929f-4bea-b9b7-cb9f40dc0786', - 'hoodeth:usar', - 'USA Rare Earth Inc.', - 18, - '0xd917b029c761d264c6a312bbbcda868658ef86a6', - UnderlyingAsset['hoodeth:usar'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - '7e955555-9504-4173-b9a6-2799b88e38f0', - 'hoodeth:dram', - 'Dataram Corporation', - 18, - '0x33c18e2cc8ae9ae486e785090d86b2ce632ff994', - UnderlyingAsset['hoodeth:dram'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), - erc20Token( - 'ea09a0ff-059b-4527-8e7b-75dfc750218b', - 'hoodeth:week', - 'Weekly T-Bill ETF', - 18, - '0xc93a8c440cea26d7445df01729f193b27965099f', - UnderlyingAsset['hoodeth:week'], - Networks.main.hoodeth, - EVM_ERC20_TOKEN_FEATURES_EXCLUDE_SINGAPORE - ), account( 'ddf32007-d3a5-4cad-9a20-b7793e96fdd2', @@ -3809,6 +3550,20 @@ export const allCoinsAndTokens = [ CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, ] ), + tip20Token( + '9dd63f8e-3f35-4d10-a623-fe7358ad66a4', + 'tempo:usdt0', + 'USDT0', + 6, + '0x20c00000000000000000000014f22ca97301eb73', + UnderlyingAsset['tempo:usdt0'], + [ + ...TEMPO_FEATURES, + CoinFeature.STABLECOIN, + CoinFeature.EVM_NON_BITGO_RECOVERY, + CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY, + ] + ), // Tempo TIP20 testnet tokens ttip20Token( 'e1872fd8-14ee-4dc9-bc5e-fd52552d9c60', @@ -3897,7 +3652,7 @@ export const allCoinsAndTokens = [ canton( '07385320-5a4f-48e9-97a5-86d4be9f24b0', 'canton', - 'Canton Coin', + 'Canton', Networks.main.canton, UnderlyingAsset.CANTON, [...CANTON_FEATURES, CoinFeature.UNSPENT_MODEL, CoinFeature.MERGE_UTXOS], @@ -3906,7 +3661,7 @@ export const allCoinsAndTokens = [ canton( 'f5d7f76b-fc5a-4da8-b1d0-a86ad0fd269e', 'tcanton', - 'Testnet Canton Coin', + 'Testnet Canton', Networks.test.canton, UnderlyingAsset.CANTON, [...CANTON_FEATURES, CoinFeature.UNSPENT_MODEL, CoinFeature.MERGE_UTXOS, CoinFeature.FANOUT_UTXOS], diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 6574cf4261..2766be0ca1 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -272,6 +272,10 @@ export enum CoinFeature { * This coin is deprecated */ DEPRECATED = 'deprecated', + /** + * This coin is sunsetting and will be deprecated in the near future, but is not deprecated yet + */ + SUNSETTING = 'sunsetting', /** * This coin is a dummy object meant to be a placeholder for an unsupported token */ @@ -1214,8 +1218,9 @@ export enum UnderlyingAsset { 'tsol:sofid' = 'tsol:sofid', 'tsol:stgsofid' = 'tsol:stgsofid', 'sol:sofid' = 'sol:sofid', - 'tsol:spcx' = 'tsol:spcx', - 'tsol:stgspcx' = 'tsol:stgspcx', + 'tsol:gospcx' = 'tsol:gospcx', + 'tsol:stggospcx' = 'tsol:stggospcx', + 'sol:gospcx' = 'sol:gospcx', 'sol:usd1' = 'sol:usd1', 'sol:usdm1' = 'sol:usdm1', 'tsol:slnd' = 'tsol:slnd', @@ -1964,6 +1969,82 @@ export enum UnderlyingAsset { 'hoodeth:usar' = 'hoodeth:usar', 'hoodeth:dram' = 'hoodeth:dram', 'hoodeth:week' = 'hoodeth:week', + 'hoodeth:aaoi' = 'hoodeth:aaoi', + 'hoodeth:amat' = 'hoodeth:amat', + 'hoodeth:apld' = 'hoodeth:apld', + 'hoodeth:arm' = 'hoodeth:arm', + 'hoodeth:asml' = 'hoodeth:asml', + 'hoodeth:asts' = 'hoodeth:asts', + 'hoodeth:avgo' = 'hoodeth:avgo', + 'hoodeth:ba' = 'hoodeth:ba', + 'hoodeth:baba' = 'hoodeth:baba', + 'hoodeth:cbrs' = 'hoodeth:cbrs', + 'hoodeth:ccl' = 'hoodeth:ccl', + 'hoodeth:celh' = 'hoodeth:celh', + 'hoodeth:clsk' = 'hoodeth:clsk', + 'hoodeth:coin' = 'hoodeth:coin', + 'hoodeth:cost' = 'hoodeth:cost', + 'hoodeth:crwd' = 'hoodeth:crwd', + 'hoodeth:ddog' = 'hoodeth:ddog', + 'hoodeth:dell' = 'hoodeth:dell', + 'hoodeth:elf' = 'hoodeth:elf', + 'hoodeth:ewy' = 'hoodeth:ewy', + 'hoodeth:f' = 'hoodeth:f', + 'hoodeth:flnc' = 'hoodeth:flnc', + 'hoodeth:futu' = 'hoodeth:futu', + 'hoodeth:gme' = 'hoodeth:gme', + 'hoodeth:glw' = 'hoodeth:glw', + 'hoodeth:inod' = 'hoodeth:inod', + 'hoodeth:intu' = 'hoodeth:intu', + 'hoodeth:ionq' = 'hoodeth:ionq', + 'hoodeth:iren' = 'hoodeth:iren', + 'hoodeth:lite' = 'hoodeth:lite', + 'hoodeth:lly' = 'hoodeth:lly', + 'hoodeth:lulu' = 'hoodeth:lulu', + 'hoodeth:lunr' = 'hoodeth:lunr', + 'hoodeth:mdb' = 'hoodeth:mdb', + 'hoodeth:mrvl' = 'hoodeth:mrvl', + 'hoodeth:mstr' = 'hoodeth:mstr', + 'hoodeth:mxl' = 'hoodeth:mxl', + 'hoodeth:nasa' = 'hoodeth:nasa', + 'hoodeth:nbis' = 'hoodeth:nbis', + 'hoodeth:nflx' = 'hoodeth:nflx', + 'hoodeth:nne' = 'hoodeth:nne', + 'hoodeth:nok' = 'hoodeth:nok', + 'hoodeth:nu' = 'hoodeth:nu', + 'hoodeth:now' = 'hoodeth:now', + 'hoodeth:nvts' = 'hoodeth:nvts', + 'hoodeth:p' = 'hoodeth:p', + 'hoodeth:peng' = 'hoodeth:peng', + 'hoodeth:poet' = 'hoodeth:poet', + 'hoodeth:pr' = 'hoodeth:pr', + 'hoodeth:qbts' = 'hoodeth:qbts', + 'hoodeth:qcom' = 'hoodeth:qcom', + 'hoodeth:qubt' = 'hoodeth:qubt', + 'hoodeth:rblx' = 'hoodeth:rblx', + 'hoodeth:rddt' = 'hoodeth:rddt', + 'hoodeth:rdw' = 'hoodeth:rdw', + 'hoodeth:rgti' = 'hoodeth:rgti', + 'hoodeth:rivn' = 'hoodeth:rivn', + 'hoodeth:rklb' = 'hoodeth:rklb', + 'hoodeth:rvi' = 'hoodeth:rvi', + 'hoodeth:sats' = 'hoodeth:sats', + 'hoodeth:shop' = 'hoodeth:shop', + 'hoodeth:smci' = 'hoodeth:smci', + 'hoodeth:sofi' = 'hoodeth:sofi', + 'hoodeth:soxx' = 'hoodeth:soxx', + 'hoodeth:spmo' = 'hoodeth:spmo', + 'hoodeth:tsem' = 'hoodeth:tsem', + 'hoodeth:tsm' = 'hoodeth:tsm', + 'hoodeth:ttwo' = 'hoodeth:ttwo', + 'hoodeth:umc' = 'hoodeth:umc', + 'hoodeth:ups' = 'hoodeth:ups', + 'hoodeth:wday' = 'hoodeth:wday', + 'hoodeth:xlk' = 'hoodeth:xlk', + 'hoodeth:xndu' = 'hoodeth:xndu', + 'hoodeth:xom' = 'hoodeth:xom', + 'hoodeth:zm' = 'hoodeth:zm', + 'hoodeth:zs' = 'hoodeth:zs', 'hemieth:hemi' = 'hemieth:hemi', 'hemieth:hemibtc' = 'hemieth:hemibtc', 'usdt0:stable' = 'usdt0:stable', @@ -2742,6 +2823,7 @@ export enum UnderlyingAsset { 'avaxc:emdx' = 'avaxc:emdx', 'avaxc:eurc' = 'avaxc:eurc', 'avaxc:ausd' = 'avaxc:ausd', + 'avaxc:mxnd' = 'avaxc:mxnd', // End FTX missing AVAXC tokens // polygon Token ERC-20 @@ -2901,6 +2983,7 @@ export enum UnderlyingAsset { 'polygon:vio0' = 'polygon:vio0', 'polygon:wots0' = 'polygon:wots0', 'polygon:mext' = 'polygon:mext', + 'polygon:mxnd' = 'polygon:mxnd', 'polygon:pert' = 'polygon:pert', 'polygon:colt' = 'polygon:colt', 'polygon:bolt' = 'polygon:bolt', @@ -3977,6 +4060,7 @@ export enum UnderlyingAsset { 'tempo:pathusd' = 'tempo:pathusd', 'tempo:usdc' = 'tempo:usdc', 'tempo:usd1' = 'tempo:usd1', + 'tempo:usdt0' = 'tempo:usdt0', // Tempo testnet tokens 'ttempo:pathusd' = 'ttempo:pathusd', diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index c7f49e2d30..4fdf41527a 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -36,6 +36,7 @@ import { CoinMap } from './map'; import { BaseNetwork, getNetwork, getNetworksMap, NetworkType } from './networks'; import { getNetworkFeatures } from './networkFeatureMapForTokens'; import { ofcErc20Coins, tOfcErc20Coins } from './coins/ofcErc20Coins'; +import { ofcHoodethTokens } from './coins/ofcHoodethTokens'; import { ofcCoins } from './coins/ofcCoins'; import { allCoinsAndTokens } from './allCoinsAndTokens'; import { botOfcTokens } from './coins/botOfcTokens'; @@ -43,6 +44,7 @@ import { botOfcTokens } from './coins/botOfcTokens'; export const coins = CoinMap.fromCoins([ ...allCoinsAndTokens, ...ofcErc20Coins, + ...ofcHoodethTokens, ...tOfcErc20Coins, ...ofcCoins, ...botOfcTokens, @@ -468,6 +470,10 @@ export function createTokenMapUsingConfigDetails(tokenConfigMap: Record): boolean { + if (coin instanceof ContractAddressDefinedToken) { + const key = CoinMap.contractAddressKey(coin); + const existing = this._coinByContractAddress.get(key); + return existing !== undefined && existing.network.type === coin.network.type; + } + if (coin instanceof NFTCollectionIdDefinedToken) { + const key = CoinMap.nftCollectionIdKey(coin); + const existing = this._coinByNftCollectionID.get(key); + return existing !== undefined && existing.network.type === coin.network.type; + } + return false; + } + static fromCoins(coins: Readonly[]): CoinMap { const coinMap = new CoinMap(); coins.forEach((coin) => { @@ -47,9 +82,25 @@ export class CoinMap { if (coin.isToken) { if (coin instanceof ContractAddressDefinedToken) { - this._coinByContractAddress.set(`${coin.family}:${coin.contractAddress}`, coin); + const contractAddressKey = CoinMap.contractAddressKey(coin); + const existingByContractAddress = this._coinByContractAddress.get(contractAddressKey); + if (existingByContractAddress) { + if (existingByContractAddress.network.type === coin.network.type) { + throw new DuplicateContractAddressDefinitionError(contractAddressKey, existingByContractAddress.name); + } + } else { + this._coinByContractAddress.set(contractAddressKey, coin); + } } else if (coin instanceof NFTCollectionIdDefinedToken) { - this._coinByNftCollectionID.set(`${coin.prefix}${coin.family}:${coin.nftCollectionId}`, coin); + const nftCollectionKey = CoinMap.nftCollectionIdKey(coin); + const existingByNftCollectionId = this._coinByNftCollectionID.get(nftCollectionKey); + if (existingByNftCollectionId) { + if (existingByNftCollectionId.network.type === coin.network.type) { + throw new DuplicateNftCollectionIdDefinitionError(nftCollectionKey, existingByNftCollectionId.name); + } + } else { + this._coinByNftCollectionID.set(nftCollectionKey, coin); + } } } } @@ -69,9 +120,9 @@ export class CoinMap { } if (oldCoin.isToken) { if (oldCoin instanceof ContractAddressDefinedToken) { - this._coinByContractAddress.delete(`${oldCoin.family}:${oldCoin.contractAddress}`); + this._coinByContractAddress.delete(CoinMap.contractAddressKey(oldCoin)); } else if (oldCoin instanceof NFTCollectionIdDefinedToken) { - this._coinByNftCollectionID.delete(`${oldCoin.prefix}${oldCoin.family}:${oldCoin.nftCollectionId}`); + this._coinByNftCollectionID.delete(CoinMap.nftCollectionIdKey(oldCoin)); } } } diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index fbed8a2a60..361c471c9f 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -1137,13 +1137,13 @@ class DydxTestnet extends Testnet implements CosmosNetwork { } class Ton extends Mainnet implements AccountNetwork { - name = 'Ton'; + name = 'Gram'; family = CoinFamily.TON; explorerUrl = 'https://tonscan.org/tx/'; } class TonTestnet extends Testnet implements AccountNetwork { - name = 'Testnet Ton'; + name = 'Testnet Gram'; family = CoinFamily.TON; explorerUrl = 'https://testnet.tonscan.org/tx/'; } @@ -2297,7 +2297,7 @@ export class FlareP extends Mainnet implements FlareNetwork { assetId = 'Flare'; name = 'FlareP'; family = CoinFamily.FLRP; - explorerUrl = 'https://flarescan.com/blockchain/pvm/transactions/'; + explorerUrl = 'https://flarescan.com/blockchain/pvm/tx/'; accountExplorerUrl = 'https://flarescan.com/blockchain/pvm/address/'; blockchainID = '11111111111111111111111111111111LpoYY'; cChainBlockchainID = '2q9e4r6Mu3U68nU1fYjgbR6JvwrRx36CohpAX5UQxse55x1Q5'; @@ -2331,7 +2331,7 @@ export class FlareP extends Mainnet implements FlareNetwork { export class FlarePTestnet extends Testnet implements FlareNetwork { name = 'FlarePTestnet'; family = CoinFamily.FLRP; - explorerUrl = 'https://coston2.testnet.flarescan.com/blockchain/pvm/transactions'; + explorerUrl = 'https://coston2.testnet.flarescan.com/blockchain/pvm/tx/'; accountExplorerUrl = 'https://coston2.testnet.flarescan.com/blockchain/pvm/address/'; flarePublicUrl = 'https://coston2.testnet.flare.network'; blockchainID = '11111111111111111111111111111111LpoYY'; diff --git a/modules/statics/test/unit/coins.ts b/modules/statics/test/unit/coins.ts index 8ebbda4fa1..d310979516 100644 --- a/modules/statics/test/unit/coins.ts +++ b/modules/statics/test/unit/coins.ts @@ -41,7 +41,7 @@ import { trimmedDynamicBaseChainConfig, } from './resources/amsTokenConfig'; import { EthLikeErc20Token } from '../../../sdk-coin-evm/src'; -import { ProgramID } from '../../src/account'; +import { ProgramID, taptNFTCollection, terc20 } from '../../src/account'; import { allCoinsAndTokens } from '../../src/allCoinsAndTokens'; interface DuplicateCoinObject { @@ -753,6 +753,70 @@ describe('CoinMap', function () { (() => CoinMap.fromCoins([btc, btc2])).should.throw(`coin with id '${btc.id}' is already defined`); }); + it('should fail to map tokens with duplicated contract address for the same family', () => { + const template = coins.get('tusdc') as Erc20Coin; + const contractAddress = template.contractAddress.toString(); + const tokenA = terc20( + '11111111-1111-4111-8111-111111111111', + 'token-a', + 'Token A', + 6, + contractAddress, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network as EthereumNetwork + ); + const tokenB = terc20( + '22222222-2222-4222-8222-222222222222', + 'token-b', + 'Token B', + 18, + contractAddress, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network as EthereumNetwork + ); + const contractAddressKey = `${tokenA.family}:${contractAddress}`; + (() => CoinMap.fromCoins([tokenA, tokenB])).should.throw( + `token with contract address '${contractAddressKey}' is already defined as 'token-a'` + ); + }); + + it('should fail to map tokens with duplicated NFT collection id for the same family', () => { + const template = coins.get('tapt:nftcollection1'); + const nftCollectionId = '0xbbc561fbfa5d105efd8dfb06ae3e7e5be46331165b99d518f094c701e40603b5'; + const tokenA = taptNFTCollection( + '11111111-1111-4111-8111-111111111111', + 'tapt:nftcollection-a', + 'NFT Collection A', + nftCollectionId, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network + ); + const tokenB = taptNFTCollection( + '22222222-2222-4222-8222-222222222222', + 'tapt:nftcollection-b', + 'NFT Collection B', + nftCollectionId, + template.asset, + template.features, + template.prefix, + template.suffix, + template.network + ); + const nftCollectionKey = `${tokenA.prefix}${tokenA.family}:${nftCollectionId}`; + (() => CoinMap.fromCoins([tokenA, tokenB])).should.throw( + `token with NFT collection id '${nftCollectionKey}' is already defined as 'tapt:nftcollection-a'` + ); + }); + it('should have iterator', function () { [...coins].length.should.be.greaterThan(100); }); @@ -1447,6 +1511,112 @@ describe('create token map using config details', () => { }); }); +describe('create token map contract address de-duplication', () => { + function firstStaticErc20(): Readonly { + for (const [, coin] of coins) { + if (coin instanceof Erc20Coin) { + return coin as Readonly; + } + } + throw new Error('expected at least one static ERC20 token in the coin map'); + } + + function collidingAmsConfig( + staticToken: Readonly, + name: string, + id: string + ): Parameters[0] { + return { + [name]: [ + { + id, + fullName: 'Colliding AMS Token', + name, + prefix: '', + suffix: name.toUpperCase(), + baseUnit: 'wei', + kind: 'crypto', + family: staticToken.family, + isToken: true, + features: [...staticToken.features], + decimalPlaces: staticToken.decimalPlaces, + asset: name, + network: staticToken.network, + primaryKeyCurve: 'secp256k1', + contractAddress: staticToken.contractAddress, + }, + ], + } as unknown as Parameters[0]; + } + + const collidingName = 'eth:cshld976colliding'; + const collidingId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'; + + it('uses a name and id not already in the static coin map', () => { + coins.has(collidingName).should.eql(false); + coins.has(collidingId).should.eql(false); + }); + + it('skips an AMS token that reuses an existing static contract address under a different name', () => { + const staticToken = firstStaticErc20(); + const config = collidingAmsConfig(staticToken, collidingName, collidingId); + + let tokenMap: CoinMap | undefined; + (() => { + tokenMap = createTokenMapUsingConfigDetails(config); + }).should.not.throw(); + + (tokenMap as CoinMap).has(collidingName).should.eql(false); + (tokenMap as CoinMap).has(staticToken.name).should.eql(true); + }); + + it('skips second AMS token that reuses a contract address already claimed by a first AMS token', () => { + const staticToken = firstStaticErc20(); + const sharedContractAddress = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + const amsNameA = 'eth:ams-only-a-976'; + const amsIdA = 'cccccccc-dddd-4eee-8fff-000000000001'; + const amsNameB = 'eth:ams-only-b-976'; + const amsIdB = 'cccccccc-dddd-4eee-8fff-000000000002'; + + const makeConfig = (name: string, id: string): Parameters[0] => + ({ + [name]: [ + { + id, + fullName: 'AMS-only Token', + name, + prefix: '', + suffix: name.toUpperCase(), + baseUnit: 'wei', + kind: 'crypto', + family: staticToken.family, + isToken: true, + features: [...staticToken.features], + decimalPlaces: staticToken.decimalPlaces, + asset: name, + network: staticToken.network, + primaryKeyCurve: 'secp256k1', + contractAddress: sharedContractAddress, + }, + ], + } as unknown as Parameters[0]); + + const config = { ...makeConfig(amsNameA, amsIdA), ...makeConfig(amsNameB, amsIdB) }; + + let tokenMap: CoinMap | undefined; + (() => { + tokenMap = createTokenMapUsingConfigDetails(config); + }).should.not.throw(); + + const map = tokenMap as CoinMap; + const hasA = map.has(amsNameA); + const hasB = map.has(amsNameB); + // Exactly one of the two AMS tokens wins; the second with the same address is skipped. + (hasA || hasB).should.eql(true, 'first AMS token should be accepted'); + (hasA && hasB).should.eql(false, 'second AMS token with duplicate address should be skipped'); + }); +}); + describe('DynamicCoin and dynamic base chain support', function () { describe('createToken with dynamic base chain', function () { it('should return a DynamicCoin when isToken is false with a BaseNetwork instance', function () { diff --git a/modules/statics/test/unit/networks.ts b/modules/statics/test/unit/networks.ts index 2b92f1d3fb..c859bd45d5 100644 --- a/modules/statics/test/unit/networks.ts +++ b/modules/statics/test/unit/networks.ts @@ -70,6 +70,17 @@ Object.entries(Networks).forEach(([category, networks]) => { Networks.test.near.accountExplorerUrl.should.equal('https://testnet.nearblocks.io/address/'); }); }); + + describe('FlareP Network', function () { + it('should have correct explorer URLs', function () { + Networks.main.flrP.explorerUrl.should.equal('https://flarescan.com/blockchain/pvm/tx/'); + Networks.main.flrP.accountExplorerUrl.should.equal('https://flarescan.com/blockchain/pvm/address/'); + Networks.test.flrP.explorerUrl.should.equal('https://coston2.testnet.flarescan.com/blockchain/pvm/tx/'); + Networks.test.flrP.accountExplorerUrl.should.equal( + 'https://coston2.testnet.flarescan.com/blockchain/pvm/address/' + ); + }); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 3a010e56b9..34cbdbad33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3492,6 +3492,78 @@ dependencies: bs58 "^5.0.0" +"@napi-rs/canvas-android-arm64@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz#b7c68b91d57702a5fc523fa82de72ea741e6766e" + integrity sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ== + +"@napi-rs/canvas-darwin-arm64@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz#d1686fa6ca699b07640efa5f45425db0a4e725e2" + integrity sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A== + +"@napi-rs/canvas-darwin-x64@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz#92978fd7eca21f7f1b59bd13ba3ce7c0e7c879a1" + integrity sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw== + +"@napi-rs/canvas-linux-arm-gnueabihf@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz#95f9892e1d5a8274871d8ee406374e9ef692bd9f" + integrity sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA== + +"@napi-rs/canvas-linux-arm64-gnu@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz#6b2b7c4bb016b8f5308115ac9ae909df4967a94b" + integrity sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw== + +"@napi-rs/canvas-linux-arm64-musl@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz#48cf6342543e4f87cf1f8e9b1aaa19ad85bcc178" + integrity sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ== + +"@napi-rs/canvas-linux-riscv64-gnu@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz#71b011007b03755c834a961735302c5837b041da" + integrity sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw== + +"@napi-rs/canvas-linux-x64-gnu@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz#3f479d3b8b8c4658e5dec1b943ad7d2eefd2811f" + integrity sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg== + +"@napi-rs/canvas-linux-x64-musl@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz#1beaca22c1fe97709a9c287115cb3c90194ddec6" + integrity sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA== + +"@napi-rs/canvas-win32-arm64-msvc@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz#8a1728b455dd17965f95c0bfdf6753be8c5851c0" + integrity sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw== + +"@napi-rs/canvas-win32-x64-msvc@0.1.100": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz#1e52883f8784ffdbe52dc2d47b05a0886aa6e9cf" + integrity sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA== + +"@napi-rs/canvas@^0.1.65": + version "0.1.100" + resolved "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz#2c89a15c28a62e1372be23fd474762ae1a8b16b7" + integrity sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA== + optionalDependencies: + "@napi-rs/canvas-android-arm64" "0.1.100" + "@napi-rs/canvas-darwin-arm64" "0.1.100" + "@napi-rs/canvas-darwin-x64" "0.1.100" + "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.100" + "@napi-rs/canvas-linux-arm64-gnu" "0.1.100" + "@napi-rs/canvas-linux-arm64-musl" "0.1.100" + "@napi-rs/canvas-linux-riscv64-gnu" "0.1.100" + "@napi-rs/canvas-linux-x64-gnu" "0.1.100" + "@napi-rs/canvas-linux-x64-musl" "0.1.100" + "@napi-rs/canvas-win32-arm64-msvc" "0.1.100" + "@napi-rs/canvas-win32-x64-msvc" "0.1.100" + "@napi-rs/wasm-runtime@0.2.4": version "0.2.4" resolved "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz" @@ -16911,6 +16983,13 @@ pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.0.9, pbkdf2@^3.1.2: sha.js "^2.4.11" to-buffer "^1.2.0" +pdfjs-dist@^4.0.0: + version "4.10.38" + resolved "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz#3ee698003790dc266cc8b55c0e662ccb9ae18f53" + integrity sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ== + optionalDependencies: + "@napi-rs/canvas" "^0.1.65" + pend@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"