From c482cb1b664eff2fa338208fb99c9840c4c33784 Mon Sep 17 00:00:00 2001 From: lbtsm Date: Tue, 18 Aug 2026 16:50:45 +0800 Subject: [PATCH 1/5] =?UTF-8?q?chore(agent):=20baseline=20=E2=80=94=20unco?= =?UTF-8?q?mmitted=20work=20from=20the=20local=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chain/sol/sync.ts | 17 ++++++++++++----- src/chain/xrp/xrp.ts | 38 +++++++++++++++++++++++++++++++++++++ src/utils/time.ts | 4 ++-- test/sol_sync_retry_test.js | 36 ++++++++++++++++++++++++++++++----- 4 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 src/chain/xrp/xrp.ts diff --git a/src/chain/sol/sync.ts b/src/chain/sol/sync.ts index a0dc756..095998c 100644 --- a/src/chain/sol/sync.ts +++ b/src/chain/sol/sync.ts @@ -12,6 +12,7 @@ const SIGNATURE_PAGE_LIMIT = 100; const SOLANA_COMMITMENT = "finalized"; const SOLANA_RETRY_ATTEMPTS = 3; const SOLANA_RETRY_DELAY_MS = 1000; +const SOLANA_GET_TRANSACTION_RETRY_DELAYS_MS = [10000, 30000, 60000]; export class SolChain { cfg: Chain; @@ -22,6 +23,8 @@ export class SolChain { mcsAddresses: string[]; retryAttempts: number; retryDelayMs: number; + getTransactionRetryDelaysMs: number[]; + sleep: (ms: number) => Promise; constructor(cfg: Chain, butter: string, butterApiKey: string) { this.cfg = cfg @@ -32,6 +35,8 @@ export class SolChain { )) this.retryAttempts = SOLANA_RETRY_ATTEMPTS this.retryDelayMs = SOLANA_RETRY_DELAY_MS + this.getTransactionRetryDelaysMs = SOLANA_GET_TRANSACTION_RETRY_DELAYS_MS + this.sleep = delay this.parser = new SolEventParser({ eventProgramIds: this.mcsAddresses }) this.handler = new SolEventHandler(cfg, butter, butterApiKey) } @@ -51,7 +56,6 @@ export class SolChain { console.log("solana catch err", err) await delay(3000) } finally { - console.log("solana filter is running") await delay(3000) } } @@ -136,7 +140,7 @@ export class SolChain { throw new Error("solana getTransaction returned null") } return trx - }, () => true) + }, () => true, this.getTransactionRetryDelaysMs) } private async handleTransactionWithRetry( @@ -155,18 +159,21 @@ export class SolChain { txHash: string, operation: () => Promise, shouldRetry: (error: Error) => boolean, + retryDelaysMs?: number[], ): Promise { let lastError: Error | undefined - for (let attempt = 1; attempt <= this.retryAttempts; attempt++) { + const maxAttempts = retryDelaysMs ? retryDelaysMs.length + 1 : this.retryAttempts + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation() } catch (err) { lastError = toError(err) - if (attempt >= this.retryAttempts || !shouldRetry(lastError)) { + if (attempt >= maxAttempts || !shouldRetry(lastError)) { break } console.log("solana retry", stage, "txHash", txHash, "attempt", attempt, "err", lastError.message) - await delay(this.retryDelayMs) + await this.sleep(retryDelaysMs ? retryDelaysMs[attempt - 1] : this.retryDelayMs) } } throw lastError || new Error(`${stage} failed`) diff --git a/src/chain/xrp/xrp.ts b/src/chain/xrp/xrp.ts new file mode 100644 index 0000000..63ad7cb --- /dev/null +++ b/src/chain/xrp/xrp.ts @@ -0,0 +1,38 @@ +import { Client, Payment, Wallet, xrpToDrops } from 'xrpl'; +import { ethers } from 'ethers'; +export async function sendXrp() { + // 1. 连接到 XRPL Testnet + const client = new Client("wss://xrplcluster.com:443") + await client.connect() + + // 2. 用测试网钱包(你也可以用自己的 seed/secret) + const wallet = Wallet.fromSeed("sEdTu8chAPYuimyWVtoeugpKgbYJX5c") // 替换成你自己的 seed + + console.log("钱包地址:", wallet.classicAddress) + + // 3. 构建 Payment 交易 + const tx: Payment = { + TransactionType: "Payment", + Account: wallet.classicAddress, + Fee: '12', // 交易费用(以 drops 为单位) + Amount: xrpToDrops("1"), // 发送 10 XRP,注意转换成 drops + Destination: "r93mC4ew8xAGZ9fsrVVsUFc2Midyd4cdYe", // 接收方地址 + Memos: [ + { + Memo: { + MemoData: "2aaebc938a5ab70e98644b0c6a8472fe02b7edece7e6e6dc71959dc34e109dfc7b226964223a34322c22746f706963223a22222c22626c6f636b5f6e756d626572223a302c2274785f68617368223a22222c2261646472223a22222c226f726465725f6964223a22307862346161353662386132353534366331386234636530633431643632653134393937313731623561393262616463333939343034653235323832343738623932222c2266726f6d223a227245503477707575456534516b4b5439624369785070517879417138326b59386951222c22746f223a22307833354437303636314639653833313733303031344533623446346234434637634533386530363334222c227372635f636861696e223a2231333630313137333538333935333933222c227372635f746f6b656e223a223078353835323530222c2273656e646572223a227245503477707575456534516b4b5439624369785070517879417138326b59386951222c22696e5f616d6f756e74223a223230303030303030222c22696e5f616d6f756e745f6e6f5f646563696d616c223a2232302e30222c22696e5f74785f68617368223a22222c226272696467655f666565223a22222c226473745f636861696e223a22313337222c226473745f746f6b656e223a22307830303030303030303030303030303030303030303030303030303030303030303030303030303030222c227265636569766572223a22307833354437303636314639653833313733303031344533623446346234434637634533386530363334222c226d6f73223a223078373837323730222c2272656c6179223a66616c73652c226d6573736167655f74797065223a332c226761735f6c696d6974223a2230222c226d696e5f6f75745f616d6f756e74223a2236313132343136313138313838383336222c22737761705f64617461223a22222c22656e7472616e6365223a2262757474657274657374227d" + } + } + ] + } + + // 4. 自动填充字段并签名、提交 + const prepared = await client.autofill(tx) + const signed = wallet.sign(prepared) + console.log("签名后的交易哈希:", signed.hash) + + const result = await client.submitAndWait(signed.tx_blob) + + console.log("提交结果:", result.result.meta) + await client.disconnect() +} \ No newline at end of file diff --git a/src/utils/time.ts b/src/utils/time.ts index 38a9cf4..ba6cfd6 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,3 +1,3 @@ -export function delay(ms: number) { +export function delay(ms: number): Promise { return new Promise( resolve => setTimeout(resolve, ms) ); -} \ No newline at end of file +} diff --git a/test/sol_sync_retry_test.js b/test/sol_sync_retry_test.js index 9994ae9..4d13056 100644 --- a/test/sol_sync_retry_test.js +++ b/test/sol_sync_retry_test.js @@ -5,7 +5,7 @@ const { SolChain } = require("../app/chain/sol/sync"); const mcsAddress = "11111111111111111111111111111111"; function createChain() { - return new SolChain( + const chain = new SolChain( { name: "solana", type: "sol", @@ -21,11 +21,12 @@ function createChain() { "http://butter", "api-key", ); + chain.sleep = async () => {}; + return chain; } async function testRetriesRpcGetTransactionErrors() { const chain = createChain(); - chain.retryDelayMs = 0; let attempts = 0; const tx = { meta: { err: null } }; const connection = { @@ -44,9 +45,35 @@ async function testRetriesRpcGetTransactionErrors() { assert.strictEqual(attempts, 3); } +async function testUsesLongerRetryDelaySequence() { + const chain = createChain(); + chain.getTransactionRetryDelaysMs = [10000, 30000, 60000]; + const delays = []; + chain.sleep = async (ms) => { + delays.push(ms); + }; + + let attempts = 0; + const tx = { meta: { err: null } }; + const connection = { + getTransaction: async () => { + attempts += 1; + if (attempts <= 3) { + throw new Error("solana getTransaction returned null"); + } + return tx; + }, + }; + + const result = await chain.getTransactionWithRetry(connection, "tx-long-retry-delay"); + + assert.strictEqual(result, tx); + assert.strictEqual(attempts, 4); + assert.deepStrictEqual(delays, [10000, 30000, 60000]); +} + async function testFailedChainTransactionAdvancesCursor() { const chain = createChain(); - chain.retryDelayMs = 0; chain.getSignaturesSince = async () => [{ signature: "failed-chain-tx" }]; chain.getTransactionWithRetry = async () => ({ meta: { err: { InstructionError: [10, "ComputationalBudgetExceeded"] } }, @@ -62,7 +89,6 @@ async function testFailedChainTransactionAdvancesCursor() { async function testHandleErrorsDoNotAdvanceCursorAfterRetries() { const chain = createChain(); - chain.retryDelayMs = 0; chain.getSignaturesSince = async () => [{ signature: "successful-chain-tx" }]; chain.getTransactionWithRetry = async () => ({ meta: { err: null } }); @@ -85,7 +111,6 @@ async function testHandleErrorsDoNotAdvanceCursorAfterRetries() { async function testMalformedButterResponseErrorsAreRetried() { const chain = createChain(); - chain.retryDelayMs = 0; chain.getSignaturesSince = async () => [{ signature: "malformed-butter-response-tx" }]; chain.getTransactionWithRetry = async () => ({ meta: { err: null } }); @@ -126,6 +151,7 @@ async function testSignaturePageLimitIs100() { async function run() { await testRetriesRpcGetTransactionErrors(); + await testUsesLongerRetryDelaySequence(); await testFailedChainTransactionAdvancesCursor(); await testHandleErrorsDoNotAdvanceCursorAfterRetries(); await testMalformedButterResponseErrorsAreRetried(); From 2caeef9db7bd29bf331494c0ef8c34d75611fef5 Mon Sep 17 00:00:00 2001 From: lbtsm Date: Tue, 18 Aug 2026 17:01:07 +0800 Subject: [PATCH 2/5] deps: declare xrpl so the tree type-checks and builds clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/chain/xrp/xrp.ts imports 'xrpl' but the package was never declared in package.json, so tsc reported TS2307 and npm run typecheck / npm run build always exited 2 — unusable as a CI gate even though tsc still emitted output. Declared as a runtime dependency rather than excluding the file from the build: xrp.ts is the start of a real XRP send path, not dead code, so keeping it in the compile scope is what preserves the type safety. xrpl@5.0.0 requires node >=20.19.0; this project runs Node 22. The lock diff is purely additive (xrpl's own subtree) — no existing package was upgraded, so the 14 known audit findings are untouched and stay out of scope here. Co-Authored-By: Claude Opus 5 (1M context) --- package-lock.json | 241 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 +- 2 files changed, 243 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3040b05..251755d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "commander": "^12.1.0", "ethers": "^6.13.5", "mysql": "^2.18.1", - "mysql2": "^3.11.0" + "mysql2": "^3.11.0", + "xrpl": "^5.0.0" }, "devDependencies": { "@types/node": "^18.13.0", @@ -159,6 +160,80 @@ "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" }, + "node_modules/@scure/base": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", + "integrity": "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.3.0.tgz", + "integrity": "sha512-mMPjNcXxJsvNveIgRIXrnwd4omu0wdXo4JowAUZO7/82+/bpAwVltclx6MG6nv2M3dA6IbfVv4xwWxaREm3OcA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.3.0", + "@noble/hashes": "2.3.0", + "@scure/base": "2.3.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", + "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.3.0.tgz", + "integrity": "sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -229,6 +304,63 @@ "@types/node": "*" } }, + "node_modules/@xrplf/isomorphic": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@xrplf/isomorphic/-/isomorphic-1.0.2.tgz", + "integrity": "sha512-ncZUdMXr6VlSXtdoiDi0jTH+gBrgGxwVeEidhoegII3PmyErbQsyj6e+j7acmR4LW/lvBkPkzb9QzRfJH0n3rA==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^2.0.1", + "eventemitter3": "5.0.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@xrplf/isomorphic/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@xrplf/isomorphic/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@xrplf/secret-numbers": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@xrplf/secret-numbers/-/secret-numbers-3.0.0.tgz", + "integrity": "sha512-qpGhAZXv5noMDjCtfzq5NK0y5rrdwTVjKhhPcAYSE+a/gogBOgqdpCKyieprVVPCnmVmJnGeRoZKBAqpCGegsA==", + "license": "ISC", + "dependencies": { + "@xrplf/isomorphic": "^1.0.2", + "ripple-keypairs": "^3.0.0" + } + }, "node_modules/aes-js": { "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", @@ -717,6 +849,12 @@ "node": "> 0.1.90" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, "node_modules/fast-stable-stringify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", @@ -1210,6 +1348,80 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ripple-address-codec": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-5.0.1.tgz", + "integrity": "sha512-JQHLKuVJV8lv9Qobmn4aUM2Dpv9WRRLKnNWfM8tN02fAbUtG8mUPsu9q9UYX8P76G4qzytEc5ZKMp/3JggNYmw==", + "license": "ISC", + "dependencies": { + "@scure/base": "^2.0.0", + "@xrplf/isomorphic": "^1.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ripple-binary-codec": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.9.0.tgz", + "integrity": "sha512-DOv4CPwm2B5mjAJNHE4tfes7tIqefi85iun8BskS7/i1PvLIwVydHIbTV5hbQ7eT69Zg4K5dtO1CzfuvQVAYvQ==", + "license": "ISC", + "dependencies": { + "@xrplf/isomorphic": "^1.0.2", + "bignumber.js": "^10.0.2", + "ripple-address-codec": "^5.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ripple-binary-codec/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" + }, + "node_modules/ripple-keypairs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-3.0.0.tgz", + "integrity": "sha512-lE69pD0E8hFNCqZoVXRyY45Yi8Ku+Qw7Rf1qRwPj4nOi34vp9NAuwzfiJH1IwXGWNCfEkwVfctG99CPTEoUf+g==", + "license": "ISC", + "dependencies": { + "@noble/curves": "^2.0.1", + "@xrplf/isomorphic": "^1.0.2", + "ripple-address-codec": "^5.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ripple-keypairs/node_modules/@noble/curves": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", + "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.3.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ripple-keypairs/node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/rpc-websockets": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.0.2.tgz", @@ -1548,6 +1760,33 @@ "optional": true } } + }, + "node_modules/xrpl": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-5.0.0.tgz", + "integrity": "sha512-YqaTFJUnhOu0mI4bsuHbKGj6w9ATcH8EIgw+gOLnh1rrlLTo5oImLQzhKJixCAPqqWOKnsY7J3jsN+l+zeEWgA==", + "license": "ISC", + "dependencies": { + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "@xrplf/isomorphic": "^1.0.2", + "@xrplf/secret-numbers": "^3.0.0", + "bignumber.js": "^10.0.2", + "eventemitter3": "^5.0.1", + "fast-json-stable-stringify": "^2.1.0", + "ripple-address-codec": "^5.0.1", + "ripple-binary-codec": "^2.8.0", + "ripple-keypairs": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/xrpl/node_modules/bignumber.js": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 7a8f495..ffa193d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "tsc", "typecheck": "tsc --noEmit", + "test": "bash scripts/run-tests.sh", "start": "node ./app/index.js", "run": "node ./app/index.js", "eslint": "eslint . --ext ts --fix" @@ -32,6 +33,7 @@ "commander": "^12.1.0", "ethers": "^6.13.5", "mysql": "^2.18.1", - "mysql2": "^3.11.0" + "mysql2": "^3.11.0", + "xrpl": "^5.0.0" } } From f4dd61cb26d111bbb67cb25e8909d748484c8af7 Mon Sep 17 00:00:00 2001 From: lbtsm Date: Tue, 18 Aug 2026 17:01:31 +0800 Subject: [PATCH 3/5] test: assert handler behaviour instead of scraping handler.ts as text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old test read src/chain/sol/handler.ts as a string and asserted that 13 exact source lines appeared in it. handler.ts has since moved 'from' to publicKeyToBytes() and the numeric fields to normalizeEventNumberHex(), so every one of those assertions was stale and the test failed on main. Rather than refresh the string literals — which would only buy time until the next refactor — the test now drives SolEventHandler.crossOut / crossIn and asserts the mos row that actually gets persisted: base58 vs raw-bytes shape for user/from, even-length unprefixed hex for chain ids and amounts, the '00' defaults when optional CrossIn fields are absent, the RefundEvent flag, the unsupported-bridgeMint alarm path, and the bridge api query. It survives a rename and it fails when the mapping is wrong, which the text version could not distinguish. Stubs replace the module exports (insertMos / requestBridgeData / alarm), the same technique mysql_duplicate_test.js already uses, so no network or database is touched. No business code was changed — the assertions were brought up to the source, not the reverse. Co-Authored-By: Claude Opus 5 (1M context) --- test/sol_handler_saved_fields_test.js | 383 ++++++++++++++++++++++++-- 1 file changed, 357 insertions(+), 26 deletions(-) diff --git a/test/sol_handler_saved_fields_test.js b/test/sol_handler_saved_fields_test.js index bddfc89..3517872 100644 --- a/test/sol_handler_saved_fields_test.js +++ b/test/sol_handler_saved_fields_test.js @@ -1,28 +1,359 @@ const assert = require("assert"); -const fs = require("fs"); -const path = require("path"); - -const handler = fs.readFileSync( - path.join(__dirname, "../src/chain/sol/handler.ts"), - "utf8", -); - -[ - 'data.set("from", normalizeEventValue(event.data.user))', - 'data.set("fromChain", normalizeEventValue(event.data.from_chain))', - 'data.set("fromChainId", normalizeEventValue(event.data.from_chain))', - 'data.set("toChain", normalizeEventValue(event.data.to_chain))', - 'data.set("tokenAmount", normalizeEventValue(event.data.bridge_amount))', - 'data.set("minAmountOut", normalizeEventValue(event.data.min_amount_out))', - 'data.set("swapTokenOutMinAmountOut", normalizeEventValue(event.data.min_amount_out))', - 'data.set("amountOut", normalizeEventValue(event.data.source_amount))', - 'data.set("bridgeAmount", normalizeEventValue(event.data.bridge_amount))', - 'data.set("amount", normalizeEventValue(event.data.amount))', - 'data.set("amountOut", normalizeEventValue(event.data.amount_out))', - 'data.set("signedMinAmountOut", normalizeEventValue(event.data.signed_min_amount_out))', - 'data.set("effectiveMinAmountOut", normalizeEventValue(event.data.effective_min_amount_out))', -].forEach((expected) => { - assert(handler.includes(expected), `missing saved field shape: ${expected}`); -}); -assert(!handler.includes('data.set("sourceAmount"')); +const { BN } = require("@project-serum/anchor"); +const { PublicKey } = require("@solana/web3.js"); +const { ethers } = require("ethers"); + +const { SolEventHandler } = require("../app/chain/sol/handler"); +const mysqlStorage = require("../app/storages/mysql/mysql"); +const butterClient = require("../app/utils/butter/butter"); +const slackAlarm = require("../app/utils/alarm/slack"); + +// Solana mainnet USDC — also the first entry of the handler's default supported bridge mints. +const BRIDGE_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); +const UNSUPPORTED_MINT = new PublicKey("mosv35NjuYkM8iMdihM96cG2ZziU2RJYZfSaGuV8uF6"); +const USER = new PublicKey("mosv35NjuYkM8iMdihM96cG2ZziU2RJYZfSaGuV8uF6"); +const DEST_TOKEN = new PublicKey("So11111111111111111111111111111111111111112"); +const EVM_TOKEN = "0x35D70661F9e831730014E3b4F4b4CF7cE38e0634"; +const EVM_RECEIVER = "0x1111111111111111111111111111111111111111"; +const MINT_DECIMALS = 6; + +const CHAIN = { + name: "solana", + type: "solana", + id: "1360108768460811", + endpoint: "https://api.mainnet-beta.solana.com", + opts: { + startBlock: "0", + mcs: "mosv35NjuYkM8iMdihM96cG2ZziU2RJYZfSaGuV8uF6", + blockConfirmations: "1", + butterEntrance: "buttertest", + }, +}; + +const TRX = { slot: 987654, blockTime: 1750000000 }; + +function bytes32WithAddressAtEnd(address) { + const out = new Uint8Array(32); + out.set(ethers.getBytes(address), 12); + return Array.from(out); +} + +function bytes33WithAddressAtEnd(address) { + // crossOut reads the receiver from bytes 13..32 of a 33 byte buffer. + const out = new Uint8Array(33); + out.set(ethers.getBytes(address), 13); + return Array.from(out); +} + +function mintAccountData(decimals) { + const data = Buffer.alloc(82); + data.writeUInt8(decimals, 44); + return data; +} + +function fakeConnection() { + return { + async getAccountInfo() { + return { data: mintAccountData(MINT_DECIMALS) }; + }, + }; +} + +// The compiled handler calls insertMos / requestBridgeData / alarm through the module +// namespace object, so replacing the exports is enough to observe them. +async function withStubs(stubs, body) { + const inserted = []; + const alarms = []; + const butterCalls = []; + + const original = { + insertMos: mysqlStorage.insertMos, + requestBridgeData: butterClient.requestBridgeData, + alarm: slackAlarm.alarm, + }; + + mysqlStorage.insertMos = async (log, callback) => { + inserted.push(log); + callback(null, inserted.length); + return 1; + }; + butterClient.requestBridgeData = async (...args) => { + butterCalls.push(args); + return stubs.bridgeData || { relay: false, data: "0x", receiver: EVM_RECEIVER }; + }; + slackAlarm.alarm = async (message) => { + alarms.push(message); + }; + + try { + return await body({ inserted, alarms, butterCalls }); + } finally { + mysqlStorage.insertMos = original.insertMos; + butterClient.requestBridgeData = original.requestBridgeData; + slackAlarm.alarm = original.alarm; + } +} + +function newHandler() { + return new SolEventHandler(CHAIN, "https://bridge.example/api", "test-api-key"); +} + +function savedLog(inserted) { + assert.strictEqual(inserted.length, 1, "expected exactly one mos row to be inserted"); + return inserted[0]; +} + +function savedFields(inserted) { + return JSON.parse(savedLog(inserted).LogData); +} + +function crossOutEvent(overrides = {}) { + return { + name: "CrossOutEvent", + data: { + order_id: Array.from({ length: 32 }, (_, i) => i + 1), + user: USER, + from_chain: new BN("1360108768460811"), + to_chain: new BN("137"), + bridge_mint: BRIDGE_MINT, + bridge_amount: new BN("20000000"), + to_token: bytes32WithAddressAtEnd(EVM_TOKEN), + receiver: bytes33WithAddressAtEnd(EVM_RECEIVER), + min_amount_out: new BN("6112416118188836"), + source_token: BRIDGE_MINT, + source_amount: new BN("19900000"), + referer_id: [9], + fee_ratio: [100], + ...overrides, + }, + }; +} + +async function testCrossOutSavedFields() { + await withStubs({}, async ({ inserted, butterCalls }) => { + const event = crossOutEvent(); + await newHandler().crossOut(event, "cross-out-tx", CHAIN.opts.mcs, fakeConnection(), TRX); + + const log = savedLog(inserted); + assert.strictEqual(log.ChainId, CHAIN.id); + assert.strictEqual(log.EventId, 98); + assert.strictEqual(log.ProjectId, 7); + assert.strictEqual(log.Topic, "crossOut"); + assert.strictEqual(log.TxHash, "cross-out-tx"); + assert.strictEqual(log.ContractAddres, CHAIN.opts.mcs); + assert.strictEqual(log.BlockNumber, TRX.slot); + assert.strictEqual(log.TxTimestamp, TRX.blockTime); + + const fields = JSON.parse(log.LogData); + + assert.strictEqual( + fields.orderId, + Buffer.from(Uint8Array.from(event.data.order_id)).toString("hex"), + ); + + // `user` keeps the base58 form; `from` is the raw 32 byte public key. + assert.strictEqual(fields.user, USER.toBase58()); + assert.deepStrictEqual(fields.from, Array.from(USER.toBytes())); + assert.strictEqual(fields.from.length, 32); + + // Chain ids and amounts are stored as even-length, unprefixed hex. + assert.strictEqual(fields.fromChain, "04d5030000000b"); + assert.strictEqual(fields.fromChainId, "04d5030000000b"); + assert.strictEqual(fields.toChain, "89"); + assert.strictEqual(fields.tokenAmount, "012fa660"); // hex(19900000), from source_amount + assert.strictEqual(fields.amountOut, "012fa660"); // hex(19900000), from source_amount + assert.strictEqual(fields.minAmountOut, "15b735c771ab24"); + assert.strictEqual(fields.swapTokenOutMinAmountOut, "15b735c771ab24"); + + // bridgeAmount stays a decimal string here (normalizeEventValue, not the hex helper). + assert.strictEqual(fields.bridgeAmount, "20000000"); + + assert.strictEqual(fields.bridgeMint, BRIDGE_MINT.toBase58()); + assert.strictEqual(fields.fromToken, BRIDGE_MINT.toBase58()); + assert.deepStrictEqual(fields.toToken, event.data.to_token); + assert.deepStrictEqual(fields.originReceiver, event.data.receiver); + assert.deepStrictEqual(fields.refererId, [9]); + assert.deepStrictEqual(fields.feeRatio, [100]); + + // The bridge api response wins for relay / swapData / receiver. + assert.strictEqual(fields.relay, false); + assert.strictEqual(fields.swapData, "0x"); + assert.strictEqual(fields.receiver, EVM_RECEIVER); + + // Renamed away from `sourceAmount` — the field must not come back. + assert.strictEqual("sourceAmount" in fields, false); + + // The bridge api is asked with the checksummed EVM token and the decimal-adjusted amount. + assert.strictEqual(butterCalls.length, 1); + const [butterUrl, butterTxHash, butterApiKey, butterQuery] = butterCalls[0]; + assert.strictEqual(butterUrl, "https://bridge.example/api"); + assert.strictEqual(butterTxHash, "cross-out-tx"); + assert.strictEqual(butterApiKey, "test-api-key"); + assert.strictEqual(butterQuery.entrance, CHAIN.opts.butterEntrance); + assert.strictEqual(butterQuery.tokenInAddress, BRIDGE_MINT.toBase58()); + assert.strictEqual(butterQuery.tokenOutAddress, ethers.getAddress(EVM_TOKEN)); + assert.strictEqual(butterQuery.amount, ethers.formatUnits(20000000n, MINT_DECIMALS)); + assert.strictEqual(butterQuery.receiver, EVM_RECEIVER); + assert.strictEqual(butterQuery.caller, USER.toBase58()); + assert.strictEqual(butterQuery.entranceId, "9"); + assert.deepStrictEqual(butterQuery.affiliate, ["9:100"]); + }); +} + +async function testCrossOutNativeTokenOutFallsBackToConfiguredAddress() { + await withStubs({}, async ({ butterCalls }) => { + const event = crossOutEvent({ to_token: Array.from({ length: 32 }, () => 0) }); + await newHandler().crossOut(event, "native-out-tx", CHAIN.opts.mcs, fakeConnection(), TRX); + + assert.strictEqual( + butterCalls[0][3].tokenOutAddress, + "0x0000000000000000000000000000000000000000", + ); + }); +} + +async function testCrossOutUnsupportedBridgeMintAlarmsAndSkipsInsert() { + await withStubs({}, async ({ inserted, alarms }) => { + const event = crossOutEvent({ bridge_mint: UNSUPPORTED_MINT }); + await newHandler().crossOut(event, "unsupported-tx", CHAIN.opts.mcs, fakeConnection(), TRX); + + assert.strictEqual(inserted.length, 0); + assert.strictEqual(alarms.length, 1); + assert(alarms[0].includes("unsupported CrossOut bridgeMint")); + assert(alarms[0].includes(UNSUPPORTED_MINT.toBase58())); + }); +} + +async function testCrossOutIgnoresOtherEvents() { + await withStubs({}, async ({ inserted }) => { + const event = crossOutEvent(); + event.name = "CrossInEvent"; + await newHandler().crossOut(event, "wrong-event-tx", CHAIN.opts.mcs, fakeConnection(), TRX); + + assert.strictEqual(inserted.length, 0); + }); +} + +async function testCrossInSavedFields() { + await withStubs({}, async ({ inserted }) => { + const event = { + name: "CrossInEvent", + data: { + order_id: Array.from({ length: 32 }, (_, i) => i), + user: USER, + from_chain: new BN("137"), + bridge_mint: BRIDGE_MINT, + bridge_amount: new BN("1500000"), + dest_token: DEST_TOKEN, + amount: new BN("1400000"), + amount_out: new BN("1350000"), + signed_min_amount_out: new BN("1300000"), + effective_min_amount_out: new BN("1250000"), + }, + }; + + await newHandler().crossIn(event, "cross-in-tx", CHAIN.opts.mcs, TRX); + + const log = savedLog(inserted); + assert.strictEqual(log.EventId, 124); + assert.strictEqual(log.Topic, "crossIn"); + + const fields = JSON.parse(log.LogData); + assert.strictEqual(fields.user, USER.toBase58()); + assert.deepStrictEqual(fields.from, Array.from(USER.toBytes())); + assert.deepStrictEqual(fields.fromToken, Array.from({ length: 32 }, () => 0)); + assert.strictEqual(fields.fromChainId, "89"); + assert.strictEqual(fields.bridgeMint, BRIDGE_MINT.toBase58()); + assert.strictEqual(fields.bridgeAmount, "16e360"); // hex(1500000) + assert.strictEqual(fields.tokenAmount, "16e360"); + assert.strictEqual(fields.destToken, DEST_TOKEN.toBase58()); + assert.deepStrictEqual(fields.toToken, Array.from(DEST_TOKEN.toBytes())); + assert.strictEqual(fields.amount, "155cc0"); // hex(1400000) + assert.strictEqual(fields.amountOut, "149970"); // hex(1350000) + assert.strictEqual(fields.signedMinAmountOut, "13d620"); // hex(1300000) + assert.strictEqual(fields.minAmountOut, "13d620"); + assert.strictEqual(fields.swapTokenOutMinAmountOut, "13d620"); + assert.strictEqual(fields.effectiveMinAmountOut, "1312d0"); // hex(1250000) + assert.strictEqual(fields.refererId, null); + assert.strictEqual(fields.feeRatio, null); + assert.strictEqual(fields.receiver, USER.toBase58()); + assert.strictEqual("isrefund" in fields, false); + }); +} + +async function testCrossInDefaultsWhenOptionalFieldsAreAbsent() { + await withStubs({}, async ({ inserted }) => { + const event = { + name: "CrossInEvent", + data: { + order_id: Array.from({ length: 32 }, () => 0), + user: USER, + bridge_mint: BRIDGE_MINT, + }, + }; + + await newHandler().crossIn(event, "cross-in-minimal-tx", CHAIN.opts.mcs, TRX); + + const fields = savedFields(inserted); + assert.strictEqual(fields.fromChainId, "00"); + assert.strictEqual(fields.tokenAmount, "00"); + assert.strictEqual(fields.minAmountOut, "00"); + assert.strictEqual(fields.swapTokenOutMinAmountOut, "00"); + // No dest_token: the bridge mint bytes are used as the destination token. + assert.deepStrictEqual(fields.toToken, Array.from(BRIDGE_MINT.toBytes())); + assert.strictEqual("bridgeAmount" in fields, false); + assert.strictEqual("amount" in fields, false); + assert.strictEqual("amountOut" in fields, false); + assert.strictEqual("effectiveMinAmountOut" in fields, false); + }); +} + +async function testRefundEventIsFlagged() { + await withStubs({}, async ({ inserted }) => { + const event = { + name: "RefundEvent", + data: { + order_id: Array.from({ length: 32 }, () => 7), + user: USER, + bridge_mint: BRIDGE_MINT, + bridge_amount: new BN("42"), + }, + }; + + await newHandler().crossIn(event, "refund-tx", CHAIN.opts.mcs, TRX); + + const fields = savedFields(inserted); + assert.strictEqual(fields.isrefund, true); + assert.strictEqual(fields.bridgeAmount, "2a"); // hex(42) + }); +} + +async function testCrossInIgnoresOtherEvents() { + await withStubs({}, async ({ inserted }) => { + const event = { + name: "CrossOutEvent", + data: { order_id: [], user: USER, bridge_mint: BRIDGE_MINT }, + }; + + await newHandler().crossIn(event, "wrong-event-tx", CHAIN.opts.mcs, TRX); + + assert.strictEqual(inserted.length, 0); + }); +} + +async function run() { + await testCrossOutSavedFields(); + await testCrossOutNativeTokenOutFallsBackToConfiguredAddress(); + await testCrossOutUnsupportedBridgeMintAlarmsAndSkipsInsert(); + await testCrossOutIgnoresOtherEvents(); + await testCrossInSavedFields(); + await testCrossInDefaultsWhenOptionalFieldsAreAbsent(); + await testRefundEventIsFlagged(); + await testCrossInIgnoresOtherEvents(); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); From 5b9dbfd9d94e65c9fd9c0f4056f750769e194800 Mon Sep 17 00:00:00 2001 From: lbtsm Date: Tue, 18 Aug 2026 17:01:31 +0800 Subject: [PATCH 4/5] test: add an npm test entry point that runs every test/ script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test/ held 7 standalone Node assert scripts with no way to run them as a set: package.json had no test script, and most of them require('../app/...') so they silently fail unless the tree is built first. scripts/run-tests.sh builds, then runs each script as its own process, prints PASS/FAIL per file plus a total, and exits non-zero if any failed — so it can gate CI. Output of passing tests is swallowed and only failures are echoed, because these scripts are very chatty on stdout. No test framework: the scripts are top-level asserts, not node:test cases, so 'exit 0 means pass' is the whole contract and jest/mocha/vitest would each require rewriting all 7. SKIP_BUILD=1 reuses an existing app/ build. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/run-tests.sh | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100755 scripts/run-tests.sh diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh new file mode 100755 index 0000000..a4706c5 --- /dev/null +++ b/scripts/run-tests.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# Runs every script under test/ as a plain Node process and reports a PASS/FAIL +# summary. The tests are top-level `assert` scripts (no test framework), so the +# contract is simply: exit 0 means pass. +# +# Most tests require("../app/...") — the compiled output — so the tree is built +# first. Set SKIP_BUILD=1 to reuse an existing app/ build. +# +set -u + +cd "$(dirname "$0")/.." + +if [ "${SKIP_BUILD:-0}" != "1" ]; then + echo "==> npm run build" + if ! npm run build; then + echo "==> build failed, not running tests" >&2 + exit 1 + fi + echo +fi + +shopt -s nullglob +tests=(test/*.js) +shopt -u nullglob + +if [ ${#tests[@]} -eq 0 ]; then + echo "==> no test files found under test/" >&2 + exit 1 +fi + +log_dir="$(mktemp -d)" +trap 'rm -rf "$log_dir"' EXIT + +passed=0 +failed=0 +failed_names=() + +echo "==> running ${#tests[@]} test(s)" +for test_file in "${tests[@]}"; do + log_file="$log_dir/$(basename "$test_file").log" + if node "$test_file" >"$log_file" 2>&1; then + echo "PASS $test_file" + passed=$((passed + 1)) + else + echo "FAIL $test_file" + failed=$((failed + 1)) + failed_names+=("$test_file") + # Only failures print their output, so a green run stays readable. + sed 's/^/ | /' "$log_file" + fi +done + +echo +echo "==> ${passed} passed, ${failed} failed, ${#tests[@]} total" + +if [ "$failed" -ne 0 ]; then + for name in "${failed_names[@]}"; do + echo "==> failed: $name" >&2 + done + exit 1 +fi From 86daf54dd2d827e79edb99739ea8a4cee03bae7f Mon Sep 17 00:00:00 2001 From: lbtsm Date: Tue, 18 Aug 2026 17:15:04 +0800 Subject: [PATCH 5/5] revert: drop the xrp chain file and the xrpl dependency WORK-20 assumed src/chain/xrp/xrp.ts was live code worth keeping in the type-check scope, so 2caeef9 declared xrpl to make the tree compile. The repo owner has since confirmed the file is not wanted: it was unfinished local scratch work (no caller anywhere in src/, no xrp branch in chain.ts) that the runtime happened to capture in the baseline commit. Deleting it removes the TS2307 at its source, so the xrpl dependency is no longer needed either. package-lock.json is restored byte-for-byte to main's version, which keeps the npm audit surface exactly as it was. Co-Authored-By: Claude Opus 5 (1M context) --- package-lock.json | 241 +------------------------------------------ package.json | 3 +- src/chain/xrp/xrp.ts | 38 ------- 3 files changed, 2 insertions(+), 280 deletions(-) delete mode 100644 src/chain/xrp/xrp.ts diff --git a/package-lock.json b/package-lock.json index 251755d..3040b05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,7 @@ "commander": "^12.1.0", "ethers": "^6.13.5", "mysql": "^2.18.1", - "mysql2": "^3.11.0", - "xrpl": "^5.0.0" + "mysql2": "^3.11.0" }, "devDependencies": { "@types/node": "^18.13.0", @@ -160,80 +159,6 @@ "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" }, - "node_modules/@scure/base": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", - "integrity": "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.3.0.tgz", - "integrity": "sha512-mMPjNcXxJsvNveIgRIXrnwd4omu0wdXo4JowAUZO7/82+/bpAwVltclx6MG6nv2M3dA6IbfVv4xwWxaREm3OcA==", - "license": "MIT", - "dependencies": { - "@noble/curves": "2.3.0", - "@noble/hashes": "2.3.0", - "@scure/base": "2.3.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", - "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.3.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.3.0.tgz", - "integrity": "sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.3.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -304,63 +229,6 @@ "@types/node": "*" } }, - "node_modules/@xrplf/isomorphic": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@xrplf/isomorphic/-/isomorphic-1.0.2.tgz", - "integrity": "sha512-ncZUdMXr6VlSXtdoiDi0jTH+gBrgGxwVeEidhoegII3PmyErbQsyj6e+j7acmR4LW/lvBkPkzb9QzRfJH0n3rA==", - "license": "ISC", - "dependencies": { - "@noble/hashes": "^2.0.1", - "eventemitter3": "5.0.1", - "ws": "^8.20.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@xrplf/isomorphic/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@xrplf/isomorphic/node_modules/ws": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", - "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@xrplf/secret-numbers": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@xrplf/secret-numbers/-/secret-numbers-3.0.0.tgz", - "integrity": "sha512-qpGhAZXv5noMDjCtfzq5NK0y5rrdwTVjKhhPcAYSE+a/gogBOgqdpCKyieprVVPCnmVmJnGeRoZKBAqpCGegsA==", - "license": "ISC", - "dependencies": { - "@xrplf/isomorphic": "^1.0.2", - "ripple-keypairs": "^3.0.0" - } - }, "node_modules/aes-js": { "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", @@ -849,12 +717,6 @@ "node": "> 0.1.90" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, "node_modules/fast-stable-stringify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", @@ -1348,80 +1210,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ripple-address-codec": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ripple-address-codec/-/ripple-address-codec-5.0.1.tgz", - "integrity": "sha512-JQHLKuVJV8lv9Qobmn4aUM2Dpv9WRRLKnNWfM8tN02fAbUtG8mUPsu9q9UYX8P76G4qzytEc5ZKMp/3JggNYmw==", - "license": "ISC", - "dependencies": { - "@scure/base": "^2.0.0", - "@xrplf/isomorphic": "^1.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/ripple-binary-codec": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.9.0.tgz", - "integrity": "sha512-DOv4CPwm2B5mjAJNHE4tfes7tIqefi85iun8BskS7/i1PvLIwVydHIbTV5hbQ7eT69Zg4K5dtO1CzfuvQVAYvQ==", - "license": "ISC", - "dependencies": { - "@xrplf/isomorphic": "^1.0.2", - "bignumber.js": "^10.0.2", - "ripple-address-codec": "^5.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/ripple-binary-codec/node_modules/bignumber.js": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", - "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", - "license": "MIT" - }, - "node_modules/ripple-keypairs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-3.0.0.tgz", - "integrity": "sha512-lE69pD0E8hFNCqZoVXRyY45Yi8Ku+Qw7Rf1qRwPj4nOi34vp9NAuwzfiJH1IwXGWNCfEkwVfctG99CPTEoUf+g==", - "license": "ISC", - "dependencies": { - "@noble/curves": "^2.0.1", - "@xrplf/isomorphic": "^1.0.2", - "ripple-address-codec": "^5.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/ripple-keypairs/node_modules/@noble/curves": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz", - "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.3.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ripple-keypairs/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/rpc-websockets": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.0.2.tgz", @@ -1760,33 +1548,6 @@ "optional": true } } - }, - "node_modules/xrpl": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-5.0.0.tgz", - "integrity": "sha512-YqaTFJUnhOu0mI4bsuHbKGj6w9ATcH8EIgw+gOLnh1rrlLTo5oImLQzhKJixCAPqqWOKnsY7J3jsN+l+zeEWgA==", - "license": "ISC", - "dependencies": { - "@scure/bip32": "^2.0.1", - "@scure/bip39": "^2.0.1", - "@xrplf/isomorphic": "^1.0.2", - "@xrplf/secret-numbers": "^3.0.0", - "bignumber.js": "^10.0.2", - "eventemitter3": "^5.0.1", - "fast-json-stable-stringify": "^2.1.0", - "ripple-address-codec": "^5.0.1", - "ripple-binary-codec": "^2.8.0", - "ripple-keypairs": "^3.0.0" - }, - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/xrpl/node_modules/bignumber.js": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", - "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", - "license": "MIT" } } } diff --git a/package.json b/package.json index ffa193d..01d186c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "commander": "^12.1.0", "ethers": "^6.13.5", "mysql": "^2.18.1", - "mysql2": "^3.11.0", - "xrpl": "^5.0.0" + "mysql2": "^3.11.0" } } diff --git a/src/chain/xrp/xrp.ts b/src/chain/xrp/xrp.ts deleted file mode 100644 index 63ad7cb..0000000 --- a/src/chain/xrp/xrp.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Client, Payment, Wallet, xrpToDrops } from 'xrpl'; -import { ethers } from 'ethers'; -export async function sendXrp() { - // 1. 连接到 XRPL Testnet - const client = new Client("wss://xrplcluster.com:443") - await client.connect() - - // 2. 用测试网钱包(你也可以用自己的 seed/secret) - const wallet = Wallet.fromSeed("sEdTu8chAPYuimyWVtoeugpKgbYJX5c") // 替换成你自己的 seed - - console.log("钱包地址:", wallet.classicAddress) - - // 3. 构建 Payment 交易 - const tx: Payment = { - TransactionType: "Payment", - Account: wallet.classicAddress, - Fee: '12', // 交易费用(以 drops 为单位) - Amount: xrpToDrops("1"), // 发送 10 XRP,注意转换成 drops - Destination: "r93mC4ew8xAGZ9fsrVVsUFc2Midyd4cdYe", // 接收方地址 - Memos: [ - { - Memo: { - MemoData: "2aaebc938a5ab70e98644b0c6a8472fe02b7edece7e6e6dc71959dc34e109dfc7b226964223a34322c22746f706963223a22222c22626c6f636b5f6e756d626572223a302c2274785f68617368223a22222c2261646472223a22222c226f726465725f6964223a22307862346161353662386132353534366331386234636530633431643632653134393937313731623561393262616463333939343034653235323832343738623932222c2266726f6d223a227245503477707575456534516b4b5439624369785070517879417138326b59386951222c22746f223a22307833354437303636314639653833313733303031344533623446346234434637634533386530363334222c227372635f636861696e223a2231333630313137333538333935333933222c227372635f746f6b656e223a223078353835323530222c2273656e646572223a227245503477707575456534516b4b5439624369785070517879417138326b59386951222c22696e5f616d6f756e74223a223230303030303030222c22696e5f616d6f756e745f6e6f5f646563696d616c223a2232302e30222c22696e5f74785f68617368223a22222c226272696467655f666565223a22222c226473745f636861696e223a22313337222c226473745f746f6b656e223a22307830303030303030303030303030303030303030303030303030303030303030303030303030303030222c227265636569766572223a22307833354437303636314639653833313733303031344533623446346234434637634533386530363334222c226d6f73223a223078373837323730222c2272656c6179223a66616c73652c226d6573736167655f74797065223a332c226761735f6c696d6974223a2230222c226d696e5f6f75745f616d6f756e74223a2236313132343136313138313838383336222c22737761705f64617461223a22222c22656e7472616e6365223a2262757474657274657374227d" - } - } - ] - } - - // 4. 自动填充字段并签名、提交 - const prepared = await client.autofill(tx) - const signed = wallet.sign(prepared) - console.log("签名后的交易哈希:", signed.hash) - - const result = await client.submitAndWait(signed.tx_blob) - - console.log("提交结果:", result.result.meta) - await client.disconnect() -} \ No newline at end of file