From 5117af93456a90dfc47a29f79018e258a3e67616 Mon Sep 17 00:00:00 2001 From: witter-deland <87846830+witter-deland@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:06:48 +0800 Subject: [PATCH] fix: tx generator (#284) --- packages/coin-kaspa/package.json | 3 +- packages/coin-kaspa/src/chain-wallet/utils.ts | 7 +- .../coin-kaspa/src/chain-wallet/wallet.ts | 366 ++++++++++-------- yarn.lock | 8 +- 4 files changed, 215 insertions(+), 169 deletions(-) diff --git a/packages/coin-kaspa/package.json b/packages/coin-kaspa/package.json index ff1465a..de02c04 100644 --- a/packages/coin-kaspa/package.json +++ b/packages/coin-kaspa/package.json @@ -33,12 +33,13 @@ "dependencies": { "@delandlabs/coin-base": "*", "@delandlabs/crypto-lib": "*", - "@kcoin/kaspa-web3.js": "^0.1.4", + "@kcoin/kaspa-web3.js": "^0.1.7", "buffer": "^6.0.3" }, "devDependencies": { "@eslint/js": "^9.16.0", "@rollup/plugin-typescript": "^12.1.1", + "@scure/bip39": "^1.5.0", "@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/parser": "^8.16.0", "eslint": "^9.16.0", diff --git a/packages/coin-kaspa/src/chain-wallet/utils.ts b/packages/coin-kaspa/src/chain-wallet/utils.ts index 9665e84..44dbcb4 100644 --- a/packages/coin-kaspa/src/chain-wallet/utils.ts +++ b/packages/coin-kaspa/src/chain-wallet/utils.ts @@ -1,9 +1,4 @@ -import { - GeneratorSettings, - SignableTransaction, - GeneratorSummary, - Generator -} from '@kcoin/kaspa-web3.js'; +import { GeneratorSettings, SignableTransaction, GeneratorSummary, Generator } from '@kcoin/kaspa-web3.js'; export const createTransactions = ( settings: GeneratorSettings diff --git a/packages/coin-kaspa/src/chain-wallet/wallet.ts b/packages/coin-kaspa/src/chain-wallet/wallet.ts index 78f90d1..efaa7a4 100644 --- a/packages/coin-kaspa/src/chain-wallet/wallet.ts +++ b/packages/coin-kaspa/src/chain-wallet/wallet.ts @@ -9,41 +9,47 @@ import { Keypair, NetworkId, RpcClient, - SendKasParams, - SendKrc20Params, - Krc20RpcClient + Krc20RpcClient, + Krc20TransferOptions, + Krc20TransferParams, + SendKasParams } from '@kcoin/kaspa-web3.js'; import { createTransactions } from './utils'; import { CHAIN, CHAIN_NAME, DERIVING_PATH, FT_ASSET, NATIVE_ASSET } from './defaults'; const AMOUNT_FOR_INSCRIBE = kaspaToSompi('0.3'); +const DEFAULT_FEES = new Fees(0n); export class KaspaChainWallet extends BaseChainWallet { private readonly networkId: NetworkId; private rpcClient: RpcClient; private krc20RpcClient: Krc20RpcClient; - private keyPair?: Keypair; constructor(chainInfo: ChainInfo, phrase: string) { - if (!chainInfo.chainId.type.equals(CHAIN)) { - throw new Error(`${CHAIN_NAME}: invalid chain type`); - } super(chainInfo, phrase); + this.validateChain(chainInfo); this.networkId = chainInfo.isMainnet ? NetworkId.Mainnet : NetworkId.Testnet10; - const endpoint = chainInfo.isMainnet - ? import.meta.env.VITE_HIBIT_KASPA_MAINNET_ENDPOINT - : import.meta.env.VITE_HIBIT_KASPA_TESTNET_ENDPOINT; this.rpcClient = new RpcClient({ networkId: this.networkId, - endpoint // ! TEMP: use endpoint for now - }); - this.krc20RpcClient = new Krc20RpcClient({ - networkId: this.networkId + endpoint: this.getEndpoint(chainInfo) }); + this.krc20RpcClient = new Krc20RpcClient({ networkId: this.networkId }); } - public override getAccount: () => Promise = async () => { + private validateChain(chainInfo: ChainInfo): void { + if (!chainInfo.chainId.type.equals(CHAIN)) { + throw new Error(`${CHAIN_NAME}: invalid chain type`); + } + } + + private getEndpoint(chainInfo: ChainInfo): string { + return chainInfo.isMainnet + ? import.meta.env.VITE_HIBIT_KASPA_MAINNET_ENDPOINT + : import.meta.env.VITE_HIBIT_KASPA_TESTNET_ENDPOINT; + } + + public override getAccount = async (): Promise => { const keypair = await this.getKeypair(); return { address: keypair.toAddress(this.networkId.networkType).toString(), @@ -51,180 +57,219 @@ export class KaspaChainWallet extends BaseChainWallet { }; }; - public override signMessage: (message: string) => Promise = async (message) => { + public override signMessage = async (message: string): Promise => { const keypair = await this.getKeypair(); const signature = keypair.signMessageWithAuxData(Buffer.from(message), new Uint8Array(32).fill(0)); return Buffer.from(signature).toString('hex'); }; - public override balanceOf = async (address: string, assetInfo: AssetInfo) => { + public override balanceOf = async (address: string, assetInfo: AssetInfo): Promise => { + this.validateAssetChain(assetInfo); + switch (assetInfo.chainAssetType) { + case NATIVE_ASSET: + return this.getNativeBalance(address, assetInfo); + case FT_ASSET: + return this.getKrc20Balance(address, assetInfo); + default: + throw new Error(`${CHAIN_NAME}: invalid chain asset type`); + } + }; + + private validateAssetChain(assetInfo: AssetInfo): void { if (!assetInfo.chain.equals(CHAIN)) { throw new Error(`${CHAIN_NAME}: invalid asset chain`); } - // native - if (assetInfo.chainAssetType.equals(NATIVE_ASSET)) { - try { - const res = await this.rpcClient.getBalanceByAddress(address); - const balance = res.balance; - return new BigNumber(balance).shiftedBy(-assetInfo.decimalPlaces.value); - } catch (e) { - console.error(e); - return new BigNumber(0); - } + } + + private async getNativeBalance(address: string, assetInfo: AssetInfo): Promise { + try { + const res = await this.rpcClient.getBalanceByAddress(address); + return new BigNumber(res.balance).shiftedBy(-assetInfo.decimalPlaces.value); + } catch (e) { + console.error(e); + return new BigNumber(0); } - // krc20 - if (assetInfo.chainAssetType.equals(FT_ASSET)) { - const res = await this.krc20RpcClient.getKrc20Balance(address, assetInfo.contractAddress); - if (res.message !== 'successful') throw new Error(`${CHAIN_NAME}: getKrc20Balance failed`); + } - for (const balanceInfo of res.result) { - if (balanceInfo.tick.toUpperCase() === assetInfo.contractAddress.toUpperCase()) { - return new BigNumber(balanceInfo.balance).shiftedBy(-Number(balanceInfo.dec)); - } + private async getKrc20Balance(address: string, assetInfo: AssetInfo): Promise { + const res = await this.krc20RpcClient.getKrc20Balance(address, assetInfo.contractAddress); + if (res.message !== 'successful') throw new Error(`${CHAIN_NAME}: getKrc20Balance failed`); + + for (const balanceInfo of res.result) { + if (balanceInfo.tick.toUpperCase() === assetInfo.contractAddress.toUpperCase()) { + return new BigNumber(balanceInfo.balance).shiftedBy(-Number(balanceInfo.dec)); } } + throw new Error(`${CHAIN_NAME}: KRC20 balance not found`); + } - throw new Error(`${CHAIN_NAME}: unsupported chain asset type ${assetInfo.chainAssetType.toString()}`); + public override transfer = async (toAddress: string, amount: BigNumber, assetInfo: AssetInfo): Promise => { + this.validateAssetChain(assetInfo); + const keypair = await this.getKeypair(); + return assetInfo.chainAssetType.equals(NATIVE_ASSET) + ? this.transferNative(toAddress, amount, assetInfo, keypair) + : this.transferKrc20(toAddress, amount, assetInfo, keypair); }; - public override transfer = async (toAddress: string, amount: BigNumber, assetInfo: AssetInfo): Promise => { - if (!assetInfo.chain.equals(CHAIN)) { - throw new Error(`${CHAIN_NAME}: invalid asset chain`); + private async transferNative( + toAddress: string, + amount: BigNumber, + assetInfo: AssetInfo, + keypair: Keypair + ): Promise { + const sendParam = new SendKasParams( + keypair.toAddress(this.networkId.networkType), + BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()), + Address.fromString(toAddress), + this.networkId, + DEFAULT_FEES + ); + const { + result: { transactions, summary } + } = await this.createTransactionsByOutputs(sendParam); + for (const tx of transactions) { + const signedTx = tx.sign([keypair.privateKey!]); + const reqMessage = signedTx.toSubmittableJsonTx(); + await this.rpcClient.submitTransaction({ + transaction: reqMessage as any, + allowOrphan: false + }); } - const keypair = await this.getKeypair(); - try { - // native - if (assetInfo.chainAssetType.equals(NATIVE_ASSET)) { - const sendParam = new SendKasParams( - keypair.toAddress(this.networkId.networkType), - BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()), - Address.fromString(toAddress), - this.networkId, - new Fees(0n) - ); - const { - result: { transactions, summary } - } = await this.createTransactionsByOutputs(sendParam); - for (const tx of transactions) { - const signedTx = tx.sign([keypair.privateKey!]); - const reqMessage = signedTx.toSubmittableJsonTx(); - await this.rpcClient.submitTransaction({ - transaction: reqMessage as any, - allowOrphan: false - }); - } - return summary.finalTransactionId?.toString() ?? ''; - } - // krc20 - if (assetInfo.chainAssetType.equals(FT_ASSET)) { - // inscribe transactions - const sendKrc20Param = new SendKrc20Params( - keypair.toAddress(this.networkId.networkType), - BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()), - toAddress, - assetInfo.contractAddress, - this.networkId, - AMOUNT_FOR_INSCRIBE, - new Fees(0n) - ); - const { - result: { transactions: commitTxs } - } = await this.createTransactionsByOutputs(sendKrc20Param); - let commitTxId = ''; - for (const commitTx of commitTxs) { - const signedTx = commitTx.sign([keypair.privateKey!]); - const reqMessage = signedTx.toSubmittableJsonTx(); - const commitRes = await this.rpcClient.submitTransaction({ - transaction: reqMessage as any, - allowOrphan: false - }); - commitTxId = commitRes.transactionId; - } - console.debug('commitTxId', commitTxId); + return summary.finalTransactionId?.toString() ?? ''; + } - const { - result: { transactions: revealTxs } - } = await this.createTransactionsByOutputs(sendKrc20Param, commitTxId); - let revealTxId = ''; - for (const revealTx of revealTxs) { - // sign - const signedTx = revealTx.sign([keypair.privateKey!], false); - const ourOutput = signedTx.transaction.tx.inputs.findIndex( - (input) => Buffer.from(input.signatureScript).toString('hex') === '' - ); - if (ourOutput !== -1) { - const signature = signedTx.transaction.createInputSignature(ourOutput, keypair.privateKey!); - const encodedSignature = sendKrc20Param.script.encodePayToScriptHashSignatureScript(signature); - signedTx.transaction.fillInputSignature(ourOutput, encodedSignature); - } - const reqMessage = signedTx.toSubmittableJsonTx(); - console.debug('reqMessage', reqMessage); - const revealRes = await this.rpcClient.submitTransaction({ - transaction: reqMessage as any, - allowOrphan: false - }); - revealTxId = revealRes.transactionId; - } - console.debug('revealTxId', revealTxId); + private async transferKrc20( + toAddress: string, + amount: BigNumber, + assetInfo: AssetInfo, + keypair: Keypair + ): Promise { + const transferOptions: Krc20TransferOptions = { + tick: assetInfo.contractAddress, + to: toAddress, + amount: BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()) + }; + const krc20TransferParams = new Krc20TransferParams( + keypair.toAddress(this.networkId.networkType), + this.networkId, + DEFAULT_FEES, + transferOptions, + DEFAULT_FEES, + AMOUNT_FOR_INSCRIBE + ); + const commitTxId = await this.processKrc20Transactions(krc20TransferParams); + return this.revealKrc20Transactions(krc20TransferParams, commitTxId, keypair); + } - return revealTxId; - } - } catch (e) { - console.error(e); - // TODO: handle error - throw e; + private async processKrc20Transactions(krc20TransferParams: Krc20TransferParams): Promise { + const { + result: { transactions: commitTxs } + } = await this.createTransactionsByOutputs(krc20TransferParams); + let commitTxId = ''; + for (const commitTx of commitTxs) { + const signedTx = commitTx.sign([this.keyPair!.privateKey!]); + const reqMessage = signedTx.toSubmittableJsonTx(); + const commitRes = await this.rpcClient.submitTransaction({ + transaction: reqMessage as any, + allowOrphan: false + }); + commitTxId = commitRes.transactionId; } + console.debug('commitTxId', commitTxId); + return commitTxId; + } - throw new Error(`${CHAIN_NAME}: unsupported chain asset type ${assetInfo.chainAssetType.toString()}`); - }; + private async revealKrc20Transactions( + krc20TransferParams: Krc20TransferParams, + commitTxId: string, + keypair: Keypair + ): Promise { + const { + result: { transactions: revealTxs } + } = await this.createTransactionsByOutputs(krc20TransferParams, commitTxId); + let revealTxId = ''; + for (const revealTx of revealTxs) { + const signedTx = revealTx.sign([keypair.privateKey!], false); + const ourOutput = signedTx.transaction.tx.inputs.findIndex( + (input) => Buffer.from(input.signatureScript).toString('hex') === '' + ); + if (ourOutput !== -1) { + const signature = signedTx.transaction.createInputSignature(ourOutput, keypair.privateKey!); + const encodedSignature = krc20TransferParams.script.encodePayToScriptHashSignatureScript(signature); + signedTx.transaction.fillInputSignature(ourOutput, encodedSignature); + } + const reqMessage = signedTx.toSubmittableJsonTx(); + console.debug('reqMessage', reqMessage); + const revealRes = await this.rpcClient.submitTransaction({ + transaction: reqMessage as any, + allowOrphan: false + }); + revealTxId = revealRes.transactionId; + } + console.debug('revealTxId', revealTxId); + return revealTxId; + } public override getEstimatedFee = async ( toAddress: string, amount: BigNumber, assetInfo: AssetInfo ): Promise => { - if (!assetInfo.chain.equals(CHAIN)) { - throw new Error(`${CHAIN_NAME}: invalid asset chain`); - } + this.validateAssetChain(assetInfo); const keypair = await this.getKeypair(); - // native - if (assetInfo.chainAssetType.equals(NATIVE_ASSET)) { - const sendKasParam = new SendKasParams( - keypair.toAddress(this.networkId.networkType), - BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()), - Address.fromString(toAddress), - this.networkId, - new Fees(0n) - ); - const { priorityFee } = await this.createTransactionsByOutputs(sendKasParam); - return new BigNumber(priorityFee.amount.toString()).shiftedBy(-assetInfo.decimalPlaces.value); - } - // krc20 - if (assetInfo.chainAssetType.equals(FT_ASSET)) { - const sendKrc20Param = new SendKrc20Params( - keypair.toAddress(this.networkId.networkType), - BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()), - toAddress, - assetInfo.contractAddress, - this.networkId, - AMOUNT_FOR_INSCRIBE, - new Fees(0n) - ); - const { priorityFee } = await this.createTransactionsByOutputs(sendKrc20Param); - return new BigNumber(priorityFee.amount.toString()).shiftedBy(-this.chainInfo.nativeAssetDecimals); - } - - throw new Error(`${CHAIN_NAME}: unsupported chain asset type ${assetInfo.chainAssetType.toString()}`); + return assetInfo.chainAssetType.equals(NATIVE_ASSET) + ? this.estimateNativeFee(toAddress, amount, assetInfo, keypair) + : this.estimateKrc20Fee(toAddress, amount, assetInfo, keypair); }; + private async estimateNativeFee( + toAddress: string, + amount: BigNumber, + assetInfo: AssetInfo, + keypair: Keypair + ): Promise { + const sendKasParam = new SendKasParams( + keypair.toAddress(this.networkId.networkType), + BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()), + Address.fromString(toAddress), + this.networkId, + DEFAULT_FEES + ); + const { priorityFee } = await this.createTransactionsByOutputs(sendKasParam); + return new BigNumber(priorityFee.amount.toString()).shiftedBy(-assetInfo.decimalPlaces.value); + } + + private async estimateKrc20Fee( + toAddress: string, + amount: BigNumber, + assetInfo: AssetInfo, + keypair: Keypair + ): Promise { + const transferOptions: Krc20TransferOptions = { + tick: assetInfo.contractAddress, + to: toAddress, + amount: BigInt(amount.shiftedBy(assetInfo.decimalPlaces.value).toString()) + }; + const krc20TransferParams = new Krc20TransferParams( + keypair.toAddress(this.networkId.networkType), + this.networkId, + DEFAULT_FEES, + transferOptions, + DEFAULT_FEES, + AMOUNT_FOR_INSCRIBE + ); + const { priorityFee } = await this.createTransactionsByOutputs(krc20TransferParams); + return new BigNumber(priorityFee.amount.toString()).shiftedBy(-this.chainInfo.nativeAssetDecimals); + } + private createTransactionsByOutputs = async ( - sendParam: SendKasParams | SendKrc20Params, + sendParam: SendKasParams | Krc20TransferParams, commitTxId?: string ): Promise<{ priorityFee: Fees; result: ReturnType; }> => { - const isKrc20Tx = sendParam instanceof SendKrc20Params; + const isKrc20Tx = sendParam instanceof Krc20TransferParams; const isReveal = commitTxId !== undefined; if (!isKrc20Tx && isReveal) { throw new Error(`${CHAIN_NAME}: invalid sendParam`); @@ -239,12 +284,17 @@ export class KaspaChainWallet extends BaseChainWallet { const settings: GeneratorSettings = !isKrc20Tx ? (sendParam as SendKasParams).toGeneratorSettings(utxos) : !isReveal - ? (sendParam as SendKrc20Params).toCommitTxGeneratorSettings(utxos) - : (sendParam as SendKrc20Params).toRevealTxGeneratorSettings(utxos, Hash.fromHex(commitTxId)); + ? (sendParam as Krc20TransferParams).toCommitTxGeneratorSettings(utxos) + : (sendParam as Krc20TransferParams).toRevealTxGeneratorSettings(utxos, Hash.fromHex(commitTxId)); const txResult = createTransactions(settings); - if (sendParam.priorityFee?.amount) { + const priorityFee = !isKrc20Tx + ? sendParam.priorityFee + : isReveal + ? sendParam.commitTxPriorityFee + : sendParam.revealPriorityFee; + if (priorityFee) { return { - priorityFee: sendParam.priorityFee, + priorityFee, result: txResult }; } diff --git a/yarn.lock b/yarn.lock index ebd5e94..2327547 100644 --- a/yarn.lock +++ b/yarn.lock @@ -628,10 +628,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@kcoin/kaspa-web3.js@^0.1.4": - version "0.1.4" - resolved "https://registry.npmmirror.com/@kcoin/kaspa-web3.js/-/kaspa-web3.js-0.1.4.tgz#2057cee8212be2e2d68fd32a219fe509172cae5d" - integrity sha512-35Ic1wk7OcwDeKPYWivGSDUvi2HpZytaJKmOafYGwiCn40j1V0yNsTdkUP7R0mI9/K+U3TTjFwzNw1/Rc8qDQg== +"@kcoin/kaspa-web3.js@^0.1.7": + version "0.1.7" + resolved "https://registry.npmmirror.com/@kcoin/kaspa-web3.js/-/kaspa-web3.js-0.1.7.tgz#edbf6b2e96a772f7751db3abdb75a8917fdf713a" + integrity sha512-Ns3b46E0idO8qAoYH+iDS7xhGRTAmS6DRqZL/5hCPccAvakgnlSuQA7RTmkqFE40oKKo7tVk7Yxdb05ge+Fi0g== dependencies: "@noble/curves" "^1.7.0" "@noble/hashes" "^1.6.1"