diff --git a/cspell-custom-words.txt b/cspell-custom-words.txt index 71a1667007..7477a4cea1 100644 --- a/cspell-custom-words.txt +++ b/cspell-custom-words.txt @@ -19,18 +19,16 @@ behaviour Berachain bigset Bigtable -Blackhole blackhole +Blackhole blackholed +Blackholed +blackholing borsh bscscan BUILDKIT bytecodes callstack -Blackhole -Blackholed -blackholing -blackholed ccqlistener CCTP celestia @@ -56,6 +54,7 @@ Cyfrin datagram denoms devnet +Dogecoin dymension Dymension ethcrypto @@ -105,6 +104,7 @@ Keccak kevm KEVM keymap +keypair keytool klaytn Klaytn @@ -169,8 +169,8 @@ readyz regen reinit reobservation -reobservations Reobservation +reobservations Reobservations reobserved repoint @@ -240,8 +240,8 @@ wormchaind Wormholescan wormscan wormscanurl +XFER xlayer xpla -XFER XPLA Zellic diff --git a/testing/dogecoin/.gitignore b/testing/dogecoin/.gitignore new file mode 100644 index 0000000000..98c9cee841 --- /dev/null +++ b/testing/dogecoin/.gitignore @@ -0,0 +1,6 @@ +/*.json +!package.json +!tsconfig.json +.env +.env.* +node_modules diff --git a/testing/dogecoin/README.md b/testing/dogecoin/README.md new file mode 100644 index 0000000000..63e88d584f --- /dev/null +++ b/testing/dogecoin/README.md @@ -0,0 +1,87 @@ +# Dogecoin Multisig Bridge Test + +This folder contains a script to test deposits and withdrawals via the Wormhole testnet guardian's 5-of-7 multisig Dogecoin Delegated Manager Set. + +## Integrator Notes + +Integrators may use Executor to relay their `UTX0` VAA v1 transfers to Dogecoin. + +For an on-chain integration on Solana, this flow might look like: + +1. Off-chain, call the Executor API to get a signed quote from Solana to Dogecoin. +2. Pass that quote to your on-chain program which emits the Wormhole message. +3. Within you program, after emitting a message, call `request_for_execution` on the Executor contract. This requires constructing a [VAA v1 request](https://github.com/wormholelabs-xyz/example-messaging-executor/blob/2061262868ed420e911a54ef619dd9b00949beb1/svm/modules/executor-requests/src/lib.rs#L7) for the VAA ID (chain, emitter, sequence number) that you just emitted. +4. Off-chain, status the request via the Executor API. + +See [withdraw-testnet-executor.ts](./withdraw-testnet-executor.ts) and the [Executor Integration Notes](https://wormholelabs.notion.site/Executor-Integration-Notes-Public-1bd3029e88cb804e8281ec19e3264c3b) for more information. + +## Testing Artifacts + +https://doge-testnet-explorer.qed.me/tx/8d55cbffc83f27ec16fb3da9c550857533169233c6683f8d97986b14b928de81 +https://doge-testnet-explorer.qed.me/tx/2916f3063844f0e2721f81041c3dc2ac02790d24ce3a1b31b8c1fb2e908d52c0 + +### Testnet Executor Example + +https://wormholelabs-xyz.github.io/executor-explorer/#/chain/1/tx/4yfrbt7q6fyW4MPojAkxmN74cH6hwcNA5to1qJZB2A14Z1Sc77URTLF9HaKMd9aFckgqSyVgD5kNqX9VpNxxHEU2?endpoint=https%3A%2F%2Fexecutor-testnet.labsapis.com&env=Testnet + +## Prerequisites + +### Bun + +```bash +curl -fsSL https://bun.sh/install | bash +``` + +### Install Dependencies + +```bash +bun ci +``` + +## Dogecoin Testnet Wallet (dogecoin-keygen.ts) + +Generates a Dogecoin testnet keypair with WIF-encoded private key and P2PKH address. + +```bash +bun run dogecoin-keygen.ts +``` + +## Solana Devnet Wallet (solana-keygen.ts) + +Generates a Solana devnet keypair. + +```bash +bun run solana-keygen.ts +``` + +Note: As the Delegated Manager Set feature is currently permissioned, in order for this test to work, your Solana devnet address has to be in the allowlist of the running testnet guardian. + +## Testnet Deposit Script (deposit-testnet.ts) + +Deposits DOGE to a P2SH multisig address controlled by the delegated manager set. The script builds a custom redeem script with embedded metadata (emitter chain, emitter contract, sub-address seed) and creates a 5-of-7 multisig. + +### Prerequisites + +- `dogecoin-testnet-keypair.json` - Funded Dogecoin wallet (from `dogecoin-keygen.ts`) +- `solana-devnet-keypair.json` - Solana keypair for emitter contract address (from `solana-keygen.ts`) + +### Usage + +```bash +bun run deposit-testnet.ts +``` + +## Testnet Withdraw Script (withdraw-testnet.ts) + +Withdraws DOGE from the P2SH multisig address back to the original sender. Collects 5-of-7 signatures from the delegated manager set to satisfy the multisig requirement. + +### Prerequisites + +- `solana-devnet-keypair.json` - Same Solana keypair used for deposit +- `deposit-info.json` - Created by running `deposit.ts` + +### Usage + +```bash +bun run withdraw-testnet.ts +``` diff --git a/testing/dogecoin/bun.lock b/testing/dogecoin/bun.lock new file mode 100644 index 0000000000..ac56fdb061 --- /dev/null +++ b/testing/dogecoin/bun.lock @@ -0,0 +1,227 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "dependencies": { + "@anchor-lang/core": "^0.32.1", + "@solana/web3.js": "^1.98.4", + "@wormhole-foundation/sdk-definitions": "^4.7.4", + "binary-layout": "^1.3.2", + "bitcoinjs-lib": "^7.0.0", + "bs58": "^6.0.0", + "ecpair": "^3.0.0", + "tiny-secp256k1": "^2.2.4", + "viem": "^2.44.1", + }, + "devDependencies": { + "@types/bun": "^1.3.5", + }, + }, + }, + "packages": { + "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], + + "@anchor-lang/borsh": ["@anchor-lang/borsh@0.32.1", "", { "dependencies": { "bn.js": "^5.1.2", "buffer-layout": "^1.2.0" }, "peerDependencies": { "@solana/web3.js": "^1.69.0" } }, "sha512-fHoqirQQiWPqQ58nZHJpQ0OXoLiLTEOiUmW1ptgtgDagsIM7WS2o1a2XHTaY/dgjJpicG90J3EElj3/KpmbuvQ=="], + + "@anchor-lang/core": ["@anchor-lang/core@0.32.1", "", { "dependencies": { "@anchor-lang/borsh": "^0.32.1", "@anchor-lang/errors": "^0.32.1", "@noble/hashes": "^1.3.1", "@solana/web3.js": "^1.69.0", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.2", "camelcase": "^6.3.0", "cross-fetch": "^3.1.5", "eventemitter3": "^4.0.7", "pako": "^2.0.3", "superstruct": "^0.15.4", "toml": "^3.0.0" } }, "sha512-UEP7z0RJ9Ic+7lLYgigHoamGaVUqtXnqJ8/1lfIM4Pgi0bAVdr7PIBJj+x4CgSqCWij9Afg8+0OYdUZWcEjCHg=="], + + "@anchor-lang/errors": ["@anchor-lang/errors@0.32.1", "", {}, "sha512-xCThjsUb4oKAYj+NZE0I/mJpGNDmhy0OSD86QZiSB7gFpYpC9He6MnWmdkRgGSPTXJsule8gSl8eoD2N6KX7Qw=="], + + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + + "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], + + "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], + + "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], + + "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], + + "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], + + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], + + "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + + "@wormhole-foundation/sdk-base": ["@wormhole-foundation/sdk-base@4.7.4", "", { "dependencies": { "@scure/base": "^1.1.3", "binary-layout": "^1.0.3" } }, "sha512-S4JvnvhYRdCb0MYELptSKSqReX3m3N2licUsp915j25M/nOm6uOp5R4sjVMjruqbbNPmNhEDz+caDg1t1Ce9yw=="], + + "@wormhole-foundation/sdk-definitions": ["@wormhole-foundation/sdk-definitions@4.7.4", "", { "dependencies": { "@noble/curves": "^1.4.0", "@noble/hashes": "^1.3.1", "@wormhole-foundation/sdk-base": "4.7.4" } }, "sha512-xeMHYIMhktX/cK9VWFFyQNrS3Fy+JccBU+76v6fRx/L5yZ1m2XeTWDFq4JROZdv4EzrZQHdHqJlTWvXr+wbJYg=="], + + "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bech32": ["bech32@2.0.0", "", {}, "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="], + + "binary-layout": ["binary-layout@1.3.2", "", {}, "sha512-Lw3GDJ7bjTQ2j6TeuwK3IGNywBKzyOPfUn17AK+FGFl8xPcX+a/IZk5f7mhnOZFsqDyTDzPuvhMreSWelAdeMg=="], + + "bip174": ["bip174@3.0.0", "", { "dependencies": { "uint8array-tools": "^0.0.9", "varuint-bitcoin": "^2.0.0" } }, "sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw=="], + + "bitcoinjs-lib": ["bitcoinjs-lib@7.0.0", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", "bip174": "^3.0.0", "bs58check": "^4.0.0", "uint8array-tools": "^0.0.9", "valibot": "^0.38.0", "varuint-bitcoin": "^2.0.0" } }, "sha512-2W6dGXFd1KG3Bs90Bzb5+ViCeSKNIYkCUWZ4cvUzUgwnneiNNZ6Sk8twGNcjlesmxC0JyLc/958QycfpvXLg7A=="], + + "bn.js": ["bn.js@5.2.2", "", {}, "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + + "bs58check": ["bs58check@4.0.0", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^6.0.0" } }, "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "buffer-layout": ["buffer-layout@1.2.2", "", {}, "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA=="], + + "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + + "ecpair": ["ecpair@3.0.0", "", { "dependencies": { "uint8array-tools": "^0.0.8", "valibot": "^0.37.0", "wif": "^5.0.0" } }, "sha512-kf4JxjsRQoD4EBzpYjGAcR0t9i/4oAeRPtyCpKvSwyotgkc6oA4E4M0/e+kep7cXe+mgxAvoeh/jdgH9h5+Wxw=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + + "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", { "peerDependencies": { "ws": "*" } }, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + + "jayson": ["jayson@4.3.0", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "stream-json": "^1.9.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "ox": ["ox@0.11.3", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw=="], + + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], + + "rpc-websockets": ["rpc-websockets@9.3.2", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-VuW2xJDnl1k8n8kjbdRSWawPRkwaVqUQNjE1TdeTawf0y0abGhtVJFTXCLfgpgGDBkO/Fj6kny8Dc/nvOW78MA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="], + + "stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="], + + "superstruct": ["superstruct@0.15.5", "", {}, "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "tiny-secp256k1": ["tiny-secp256k1@2.2.4", "", { "dependencies": { "uint8array-tools": "0.0.7" } }, "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q=="], + + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uint8array-tools": ["uint8array-tools@0.0.9", "", {}, "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A=="], + + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "valibot": ["valibot@0.38.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-RCJa0fetnzp+h+KN9BdgYOgtsMAG9bfoJ9JSjIhFHobKWVWyzM3jjaeNTdpFK9tQtf3q1sguXeERJ/LcmdFE7w=="], + + "varuint-bitcoin": ["varuint-bitcoin@2.0.0", "", { "dependencies": { "uint8array-tools": "^0.0.8" } }, "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog=="], + + "viem": ["viem@2.44.1", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.3", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-wfQda7PIJeF9hWiGKV628AfBwyYxyoc89OcgF4s4/chs2PY8ibUA7IG+DWdU4oAkjhHm9w47w1m2dwwOPBU+ng=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "wif": ["wif@5.0.0", "", { "dependencies": { "bs58check": "^4.0.0" } }, "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "@anchor-lang/core/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "@solana/errors/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + + "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "@solana/web3.js/superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "borsh/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "ecpair/uint8array-tools": ["uint8array-tools@0.0.8", "", {}, "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g=="], + + "ecpair/valibot": ["valibot@0.37.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ=="], + + "jayson/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "rpc-websockets/@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "rpc-websockets/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "rpc-websockets/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "tiny-secp256k1/uint8array-tools": ["uint8array-tools@0.0.7", "", {}, "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ=="], + + "varuint-bitcoin/uint8array-tools": ["uint8array-tools@0.0.8", "", {}, "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g=="], + + "viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "@anchor-lang/core/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "@solana/web3.js/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + + "borsh/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + } +} diff --git a/testing/dogecoin/deposit-testnet.ts b/testing/dogecoin/deposit-testnet.ts new file mode 100644 index 0000000000..783dfebda1 --- /dev/null +++ b/testing/dogecoin/deposit-testnet.ts @@ -0,0 +1,161 @@ +import * as bitcoin from "bitcoinjs-lib"; +import { loadManagerKeys } from "./manager"; +import { + ECPair, + EMITTER_CHAIN, + buildRedeemScript, + dogecoinTestnet, + loadSolanaEmitterContract, +} from "./redeem-script"; +import { + DOGECOIN_CHAIN_ID, + FEE, + KOINU_PER_DOGE, + broadcastTx, + explorerTxUrl, + fetchRawTx, + fetchUtxos, +} from "./shared"; + +// Load emitter contract and manager keys +const emitterContract = await loadSolanaEmitterContract(); + +const { + mThreshold, + nTotal, + pubkeys: managerPubkeys, +} = await loadManagerKeys(DOGECOIN_CHAIN_ID); + +console.log("\nManager keys:"); +for (let i = 0; i < managerPubkeys.length; i++) { + console.log(` ${i}: ${managerPubkeys[i]!.toString("hex")}`); +} + +// Build the redeem script (using Solana pubkey as recipient_address) +const redeemScript = buildRedeemScript({ + emitterChain: EMITTER_CHAIN, + emitterContract, + recipientAddress: emitterContract, // Use Solana public key as recipient_address + managerPubkeys, + mThreshold, + nTotal, +}); + +console.log("\nRedeem script length:", redeemScript.length, "bytes"); +console.log("Redeem script (hex):", redeemScript.toString("hex")); + +// Generate P2SH address from redeem script +const p2sh = bitcoin.payments.p2sh({ + redeem: { output: new Uint8Array(redeemScript) }, + network: dogecoinTestnet, +}); + +console.log("\nP2SH Address:", p2sh.address); +console.log( + "Script Hash:", + p2sh.hash ? Buffer.from(p2sh.hash).toString("hex") : undefined, +); + +// Read sender's keypair +const senderKeypair = await Bun.file("dogecoin-testnet-keypair.json").json(); +const senderKeyPair = ECPair.fromWIF( + senderKeypair.privateKeyWIF, + dogecoinTestnet, +); +const senderAddress = senderKeypair.address; + +console.log("\nSender address:", senderAddress); + +// Amount to send (in koinu) - 1 DOGE = 100,000,000 koinu +const AMOUNT_TO_SEND = KOINU_PER_DOGE; // 1 DOGE + +console.log("\nFetching UTXOs..."); +const utxos = await fetchUtxos(senderAddress); +console.log(`Found ${utxos.length} UTXOs`); + +if (utxos.length === 0) { + console.error("No UTXOs available. Please fund the sender address first."); + console.log(`Get testnet coins from: https://faucet.doge.toys/`); + process.exit(1); +} + +// Calculate total available +const totalAvailable = utxos.reduce( + (sum: number, utxo: any) => sum + utxo.value, + 0, +); +console.log(`Total available: ${totalAvailable / KOINU_PER_DOGE} DOGE`); + +if (totalAvailable < AMOUNT_TO_SEND + FEE) { + console.error( + `Insufficient funds. Need ${(AMOUNT_TO_SEND + FEE) / KOINU_PER_DOGE} DOGE`, + ); + process.exit(1); +} + +// Build transaction +const psbt = new bitcoin.Psbt({ network: dogecoinTestnet }); + +// Add inputs +let inputSum = 0; +for (const utxo of utxos) { + // Fetch raw transaction for the UTXO + const rawTxHex = await fetchRawTx(utxo.txid); + + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + nonWitnessUtxo: new Uint8Array(Buffer.from(rawTxHex, "hex")), + }); + + inputSum += utxo.value; + if (inputSum >= AMOUNT_TO_SEND + FEE) break; +} + +// Add output to P2SH address +psbt.addOutput({ + address: p2sh.address!, + value: BigInt(AMOUNT_TO_SEND), +}); + +// Add change output if needed +const change = inputSum - AMOUNT_TO_SEND - FEE; +if (change > 0) { + psbt.addOutput({ + address: senderAddress, + value: BigInt(change), + }); +} + +// Sign all inputs +psbt.signAllInputs(senderKeyPair); +psbt.finalizeAllInputs(); + +// Extract and broadcast +const tx = psbt.extractTransaction(); +const txHex = tx.toHex(); + +console.log("\nTransaction hex:", txHex); +console.log("Transaction ID:", tx.getId()); + +console.log("\nBroadcasting transaction..."); +try { + const txid = await broadcastTx(txHex); + console.log("Transaction broadcast successfully!"); + console.log("TXID:", txid); + console.log(`\nView on explorer: ${explorerTxUrl(txid)}`); + + // Save deposit info + const depositInfo = { + txid: tx.getId(), + amount: AMOUNT_TO_SEND, + p2shAddress: p2sh.address, + redeemScript: redeemScript.toString("hex"), + senderAddress, + timestamp: new Date().toISOString(), + }; + await Bun.write("deposit-info.json", JSON.stringify(depositInfo, null, 2)); + console.log("\nDeposit info saved to deposit-info.json"); +} catch (error) { + console.error("Broadcast failed:", error); +} diff --git a/testing/dogecoin/dogecoin-keygen.ts b/testing/dogecoin/dogecoin-keygen.ts new file mode 100644 index 0000000000..01652cb268 --- /dev/null +++ b/testing/dogecoin/dogecoin-keygen.ts @@ -0,0 +1,50 @@ +import * as bitcoin from "bitcoinjs-lib"; +import * as ecc from "tiny-secp256k1"; +import { ECPairFactory } from "ecpair"; + +bitcoin.initEccLib(ecc); +const ECPair = ECPairFactory(ecc); + +// Dogecoin Testnet network parameters +const dogecoinTestnet: bitcoin.Network = { + messagePrefix: "\x19Dogecoin Signed Message:\n", + bech32: "tdge", + bip32: { + public: 0x043587cf, + private: 0x04358394, + }, + pubKeyHash: 0x71, // addresses start with 'n' + scriptHash: 0xc4, + wif: 0xf1, // WIF prefix for testnet +}; + +// Generate a new keypair +const keyPair = ECPair.makeRandom({ network: dogecoinTestnet }); + +// Get private key in WIF format +const privateKeyWIF = keyPair.toWIF(); + +// Get public key +const publicKey = Buffer.from(keyPair.publicKey).toString("hex"); + +// Generate address (P2PKH) +const { address } = bitcoin.payments.p2pkh({ + pubkey: new Uint8Array(Buffer.from(keyPair.publicKey)), + network: dogecoinTestnet, +}); + +// Save to JSON file +const keypairData = { + network: "dogecoin-testnet", + privateKeyWIF, + publicKey, + address, + generatedAt: new Date().toISOString(), +}; + +await Bun.write( + "dogecoin-testnet-keypair.json", + JSON.stringify(keypairData, null, 2), +); + +console.log("Keypair saved to dogecoin-testnet-keypair.json"); diff --git a/testing/dogecoin/idls/executor.json b/testing/dogecoin/idls/executor.json new file mode 100644 index 0000000000..7878cc25ff --- /dev/null +++ b/testing/dogecoin/idls/executor.json @@ -0,0 +1,119 @@ +{ + "address": "execXUrAsMnqMmTHj5m7N1YQgsDz3cwGLYCYyuDRciV", + "metadata": { + "name": "executor", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "request_for_execution", + "discriminator": [ + 109, + 107, + 87, + 37, + 151, + 192, + 119, + 115 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "payee", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "RequestForExecutionArgs" + } + } + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidArguments", + "msg": "InvalidArguments" + }, + { + "code": 6001, + "name": "QuoteSrcChainMismatch", + "msg": "QuoteSrcChainMismatch" + }, + { + "code": 6002, + "name": "QuoteDstChainMismatch", + "msg": "QuoteDstChainMismatch" + }, + { + "code": 6003, + "name": "QuoteExpired", + "msg": "QuoteExpired" + }, + { + "code": 6004, + "name": "QuotePayeeMismatch", + "msg": "QuotePayeeMismatch" + } + ], + "types": [ + { + "name": "RequestForExecutionArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "dst_chain", + "type": "u16" + }, + { + "name": "dst_addr", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "refund_addr", + "type": "pubkey" + }, + { + "name": "signed_quote_bytes", + "type": "bytes" + }, + { + "name": "request_bytes", + "type": "bytes" + }, + { + "name": "relay_instructions", + "type": "bytes" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/testing/dogecoin/idls/executor.ts b/testing/dogecoin/idls/executor.ts new file mode 100644 index 0000000000..20efce8414 --- /dev/null +++ b/testing/dogecoin/idls/executor.ts @@ -0,0 +1,125 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/executor.json`. + */ +export type Executor = { + "address": "execXUrAsMnqMmTHj5m7N1YQgsDz3cwGLYCYyuDRciV", + "metadata": { + "name": "executor", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "requestForExecution", + "discriminator": [ + 109, + 107, + 87, + 37, + 151, + 192, + 119, + 115 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "payee", + "writable": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": { + "name": "requestForExecutionArgs" + } + } + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "invalidArguments", + "msg": "invalidArguments" + }, + { + "code": 6001, + "name": "quoteSrcChainMismatch", + "msg": "quoteSrcChainMismatch" + }, + { + "code": 6002, + "name": "quoteDstChainMismatch", + "msg": "quoteDstChainMismatch" + }, + { + "code": 6003, + "name": "quoteExpired", + "msg": "quoteExpired" + }, + { + "code": 6004, + "name": "quotePayeeMismatch", + "msg": "quotePayeeMismatch" + } + ], + "types": [ + { + "name": "requestForExecutionArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "dstChain", + "type": "u16" + }, + { + "name": "dstAddr", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "refundAddr", + "type": "pubkey" + }, + { + "name": "signedQuoteBytes", + "type": "bytes" + }, + { + "name": "requestBytes", + "type": "bytes" + }, + { + "name": "relayInstructions", + "type": "bytes" + } + ] + } + } + ] +}; diff --git a/testing/dogecoin/manager.ts b/testing/dogecoin/manager.ts new file mode 100644 index 0000000000..3125771ac5 --- /dev/null +++ b/testing/dogecoin/manager.ts @@ -0,0 +1,75 @@ +// https://sepolia.etherscan.io/address/0x086a699900262d829512299abe07648870000dd1#readContract +export const M_THRESHOLD = 5; +export const N_TOTAL = 7; +const currentManagerSet = + "0x01050702349de56ca5dd06db8660419d6f150662e0f04febdbf6512d7cfe78c23b51491c035163bfd9518b0a536a17f330a1589fe21d7404b51f525a0a990a65a701952ebb036d40b0b85bca49e41f05a26950578bb13a424507ce34a80f83d3cf601e25818b0307681002ae28b9399e828d0f46d54c31d5d6ff187b3bdddc6615987a466455f50375abc8955c8a8c875ee1febd157132adcc1b992d69a946e83485b8360e23a277030212d206546216917a75533ed6c975f8f794ba0d8a7fb84dedf65ebb20e64841037ff483369b52bd87a73f23413dd8fcace71de7f7823c5c9120f1e9cfe5733a88"; +const managerPubkeys = currentManagerSet + .substring(8) + .match(/.{66}/g)! + .map((x) => Buffer.from(x, "hex")); +// TODO: read this dynamically and correctly from the contract +export async function loadManagerKeys(chainId: number) { + return { mThreshold: M_THRESHOLD, nTotal: N_TOTAL, pubkeys: managerPubkeys }; +} + +// Manager signatures response type +export interface ManagerSignaturesResponse { + vaaHash: string; + vaaId: string; + destinationChain: number; + managerSetIndex: number; + required: number; + total: number; + isComplete: boolean; + signatures: { + signerIndex: number; + signatures: string[]; // base64-encoded DER signatures + }[]; +} + +// Fetch signatures from Guardian Manager RPC +export async function fetchManagerSignatures( + guardianRpc: string, + emitterChain: number, + emitterAddress: string, + sequence: bigint, + maxRetries: number = 60, + retryDelayMs: number = 2000, +): Promise { + const url = `${guardianRpc}/v1/manager/signed_vaa/${emitterChain}/${emitterAddress}/${sequence}`; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + console.log( + `Fetching manager signatures (attempt ${attempt}/${maxRetries}): ${url}`, + ); + + try { + const response = await fetch(url); + if (response.ok) { + const data = (await response.json()) as ManagerSignaturesResponse; + if (data.isComplete) { + console.log("Signatures complete!"); + console.log(` Required: ${data.required}/${data.total}`); + console.log(` Signatures collected: ${data.signatures.length}`); + return data; + } else { + console.log( + `Signatures incomplete: ${data.signatures.length}/${data.required} required`, + ); + } + } else { + const text = await response.text(); + console.log(`Response ${response.status}: ${text}`); + } + } catch (error) { + console.log(`Fetch error: ${error}`); + } + + if (attempt < maxRetries) { + console.log(`Signatures not ready, waiting ${retryDelayMs / 1000}s...`); + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + } + } + + throw new Error("Failed to fetch manager signatures after max retries"); +} diff --git a/testing/dogecoin/package.json b/testing/dogecoin/package.json new file mode 100644 index 0000000000..214c3fd999 --- /dev/null +++ b/testing/dogecoin/package.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "@anchor-lang/core": "^0.32.1", + "@solana/web3.js": "^1.98.4", + "@wormhole-foundation/sdk-definitions": "^4.7.4", + "binary-layout": "^1.3.2", + "bitcoinjs-lib": "^7.0.0", + "bs58": "^6.0.0", + "ecpair": "^3.0.0", + "tiny-secp256k1": "^2.2.4", + "viem": "^2.44.1" + }, + "devDependencies": { + "@types/bun": "^1.3.5" + } +} diff --git a/testing/dogecoin/redeem-script.ts b/testing/dogecoin/redeem-script.ts new file mode 100644 index 0000000000..bd416cbdd9 --- /dev/null +++ b/testing/dogecoin/redeem-script.ts @@ -0,0 +1,86 @@ +import * as bitcoin from "bitcoinjs-lib"; +import * as ecc from "tiny-secp256k1"; +import { ECPairFactory } from "ecpair"; +import bs58 from "bs58"; + +bitcoin.initEccLib(ecc); +export const ECPair = ECPairFactory(ecc); + +// Dogecoin Testnet network parameters +export const dogecoinTestnet: bitcoin.Network = { + messagePrefix: "\x19Dogecoin Signed Message:\n", + bech32: "tdge", + bip32: { + public: 0x043587cf, + private: 0x04358394, + }, + pubKeyHash: 0x71, + scriptHash: 0xc4, + wif: 0xf1, +}; + +// Redeem script constants +export const EMITTER_CHAIN = 1; // u16 + +export interface RedeemScriptParams { + emitterChain: number; + emitterContract: string; // 32 bytes hex + recipientAddress: string; // 32 bytes hex + managerPubkeys: Buffer[]; + mThreshold: number; + nTotal: number; +} + +// Helper to get OP_N opcode for small integers (1-16) +function opN(n: number): number { + if (n < 1 || n > 16) throw new Error(`Invalid OP_N value: ${n}`); + return bitcoin.opcodes.OP_1 + (n - 1); +} + +// Build the custom redeem script +export function buildRedeemScript(params: RedeemScriptParams): Buffer { + const { + emitterChain, + emitterContract, + recipientAddress, + managerPubkeys, + mThreshold, + nTotal, + } = params; + + // Prepare data buffers + const emitterChainBuf = Buffer.alloc(2); + emitterChainBuf.writeUInt16BE(emitterChain); + const emitterContractBuf = Buffer.from(emitterContract, "hex"); + const recipientAddressBuf = Buffer.from(recipientAddress, "hex"); + + // Build script using bitcoin.script.compile for proper push opcodes + const compiled = bitcoin.script.compile([ + // Push emitter_chain (2 bytes, u16 BE) + new Uint8Array(emitterChainBuf), + // Push emitter_contract (32 bytes) + new Uint8Array(emitterContractBuf), + // OP_2DROP - drops top 2 stack items + bitcoin.opcodes.OP_2DROP, + // Push recipient_address (32 bytes) + new Uint8Array(recipientAddressBuf), + // OP_DROP - drops top stack item + bitcoin.opcodes.OP_DROP, + // OP_M (threshold) + opN(mThreshold), + // Push each pubkey in index order (33 bytes compressed secp256k1) + ...managerPubkeys.map((pk) => new Uint8Array(pk)), + // OP_N (total keys) + opN(nTotal), + // OP_CHECKMULTISIG + bitcoin.opcodes.OP_CHECKMULTISIG, + ]); + + return Buffer.from(compiled); +} + +// Load Solana keypair and derive emitter contract +export async function loadSolanaEmitterContract(): Promise { + const solanaKeypair = await Bun.file("solana-devnet-keypair.json").json(); + return Buffer.from(bs58.decode(solanaKeypair.publicKey)).toString("hex"); +} diff --git a/testing/dogecoin/requestForExecution.ts b/testing/dogecoin/requestForExecution.ts new file mode 100644 index 0000000000..6f3d51bff6 --- /dev/null +++ b/testing/dogecoin/requestForExecution.ts @@ -0,0 +1,26 @@ +import { RequestPrefix } from "@wormhole-foundation/sdk-definitions"; +import type { DeriveType, Layout } from "binary-layout"; + +// Binary layout for the VAA v1 request in https://github.com/wormholelabs-xyz/example-messaging-executor?tab=readme-ov-file#vaa-v1-request +// As of this writing, it is not yet available in https://github.com/wormhole-foundation/wormhole-sdk-ts/tree/main/core/definitions/src/protocols/executor +// On-chain on SVM, one could use https://github.com/wormholelabs-xyz/example-messaging-executor/blob/2061262868ed420e911a54ef619dd9b00949beb1/svm/modules/executor-requests/src/lib.rs#L7 + +export const vaaV1RequestLayout = [ + { name: "chain", binary: "uint", size: 2 }, + { name: "address", binary: "bytes", size: 32 }, + { name: "sequence", binary: "uint", size: 8 }, +] as const satisfies Layout; + +export type VAAv1Request = DeriveType; + +export const requestLayout = [ + { + name: "request", + binary: "switch", + idSize: 4, + idTag: "prefix", + layouts: [[[0x45525631, RequestPrefix.ERV1], vaaV1RequestLayout]], + }, +] as const satisfies Layout; + +export type RequestLayout = DeriveType; diff --git a/testing/dogecoin/shared.ts b/testing/dogecoin/shared.ts new file mode 100644 index 0000000000..55590a7868 --- /dev/null +++ b/testing/dogecoin/shared.ts @@ -0,0 +1,47 @@ +export const DOGECOIN_CHAIN_ID = 65; + +// API endpoints +export const ELECTRS_API = "https://doge-electrs-testnet-demo.qed.me"; +export const EXPLORER_URL = "https://doge-testnet-explorer.qed.me"; + +// Transaction constants +export const FEE = 1_000_000; // 0.01 DOGE fee +export const KOINU_PER_DOGE = 100_000_000; + +// Fetch UTXOs for an address +export async function fetchUtxos(address: string): Promise { + const response = await fetch(`${ELECTRS_API}/address/${address}/utxo`); + console.log(response); + if (!response.ok) { + throw new Error(`Failed to fetch UTXOs: ${response.statusText}`); + } + return response.json(); +} + +// Fetch raw transaction hex +export async function fetchRawTx(txid: string): Promise { + const response = await fetch(`${ELECTRS_API}/tx/${txid}/hex`); + if (!response.ok) { + throw new Error(`Failed to fetch raw tx: ${response.statusText}`); + } + return response.text(); +} + +// Broadcast transaction +export async function broadcastTx(txHex: string): Promise { + const response = await fetch(`${ELECTRS_API}/tx`, { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: txHex, + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to broadcast: ${error}`); + } + return response.text(); +} + +// Get explorer URL for a transaction +export function explorerTxUrl(txid: string): string { + return `${EXPLORER_URL}/tx/${txid}`; +} diff --git a/testing/dogecoin/solana-keygen.ts b/testing/dogecoin/solana-keygen.ts new file mode 100644 index 0000000000..9690468e44 --- /dev/null +++ b/testing/dogecoin/solana-keygen.ts @@ -0,0 +1,31 @@ +import { Keypair } from "@solana/web3.js"; +import bs58 from "bs58"; + +// Generate a new keypair +const keypair = Keypair.generate(); + +// Get private key in base58 format (common format for Solana wallets) +const privateKeyBase58 = bs58.encode(keypair.secretKey); + +// Get private key as byte array (used by Solana CLI) +const privateKeyArray = Array.from(keypair.secretKey); + +// Get public key (this is also the address in Solana) +const publicKey = keypair.publicKey.toBase58(); + +// Save to JSON file +const keypairData = { + network: "solana-devnet", + privateKeyBase58, + privateKeyArray, + publicKey, + address: publicKey, // In Solana, address and public key are the same + generatedAt: new Date().toISOString(), +}; + +await Bun.write( + "solana-devnet-keypair.json", + JSON.stringify(keypairData, null, 2), +); + +console.log("Keypair saved to solana-devnet-keypair.json"); diff --git a/testing/dogecoin/tsconfig.json b/testing/dogecoin/tsconfig.json new file mode 100644 index 0000000000..ec6d698129 --- /dev/null +++ b/testing/dogecoin/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "types": ["bun-types"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/testing/dogecoin/vaa.ts b/testing/dogecoin/vaa.ts new file mode 100644 index 0000000000..ac23fc0ea2 --- /dev/null +++ b/testing/dogecoin/vaa.ts @@ -0,0 +1,219 @@ +// VAA payload encoding/decoding utilities + +// Address types for output +export const ADDRESS_TYPE_P2PKH = 0; +export const ADDRESS_TYPE_P2SH = 1; + +export interface UnlockPayloadInput { + originalRecipientAddress: string; // 32 bytes hex + transactionId: string; // 32 bytes hex + vout: number; +} + +export interface UnlockPayloadOutput { + amount: bigint; + addressType: number; + address: string; // hex +} + +export interface UnlockPayload { + destinationChain: number; + delegatedManagerSet: number; + inputs: UnlockPayloadInput[]; + outputs: UnlockPayloadOutput[]; +} + +export interface ParsedVAA { + version: number; + guardianSetIndex: number; + signatures: { guardianIndex: number; signature: Buffer }[]; + timestamp: number; + nonce: number; + emitterChain: number; + emitterAddress: string; + sequence: bigint; + consistencyLevel: number; + payload: Buffer; +} + +// Encode the VAA payload for Dogecoin unlock +export function encodeUnlockPayload(params: UnlockPayload): Buffer { + const parts: Buffer[] = []; + + // Prefix "UTX0" (4 bytes) + parts.push(Buffer.from("UTX0", "ascii")); + + // destination_chain (uint16 BE) + const destChainBuf = Buffer.alloc(2); + destChainBuf.writeUInt16BE(params.destinationChain); + parts.push(destChainBuf); + + // delegated_manager_set (uint32 BE) + const managerSetBuf = Buffer.alloc(4); + managerSetBuf.writeUInt32BE(params.delegatedManagerSet); + parts.push(managerSetBuf); + + // len_input (uint32 BE) + const lenInputBuf = Buffer.alloc(4); + lenInputBuf.writeUInt32BE(params.inputs.length); + parts.push(lenInputBuf); + + // inputs + for (const input of params.inputs) { + // original_recipient_address (32 bytes) + parts.push(Buffer.from(input.originalRecipientAddress, "hex")); + // transaction_id (32 bytes) + parts.push(Buffer.from(input.transactionId, "hex")); + // vout (uint32 BE) + const voutBuf = Buffer.alloc(4); + voutBuf.writeUInt32BE(input.vout); + parts.push(voutBuf); + } + + // len_output (uint32 BE) + const lenOutputBuf = Buffer.alloc(4); + lenOutputBuf.writeUInt32BE(params.outputs.length); + parts.push(lenOutputBuf); + + // outputs + for (const output of params.outputs) { + // amount (uint64 BE) + const amountBuf = Buffer.alloc(8); + amountBuf.writeBigUInt64BE(output.amount); + parts.push(amountBuf); + // address_type (uint32 BE) + const addrTypeBuf = Buffer.alloc(4); + addrTypeBuf.writeUInt32BE(output.addressType); + parts.push(addrTypeBuf); + // address (length determined by address_type: 20 for P2PKH/P2SH, 32 for P2WSH/P2TR) + const addrBuf = Buffer.from(output.address, "hex"); + parts.push(addrBuf); + } + + return Buffer.concat(parts.map((p) => new Uint8Array(p))); +} + +// Decode VAA payload +export function decodeUnlockPayload(payload: Buffer): UnlockPayload { + let offset = 0; + + // Prefix (4 bytes) + const prefix = payload.subarray(offset, offset + 4).toString("ascii"); + if (prefix !== "UTX0") { + throw new Error(`Invalid payload prefix: ${prefix}`); + } + offset += 4; + + // destination_chain (uint16 BE) + const destinationChain = payload.readUInt16BE(offset); + offset += 2; + + // delegated_manager_set (uint32 BE) + const delegatedManagerSet = payload.readUInt32BE(offset); + offset += 4; + + // len_input (uint32 BE) + const lenInput = payload.readUInt32BE(offset); + offset += 4; + + // inputs + const inputs: UnlockPayloadInput[] = []; + for (let i = 0; i < lenInput; i++) { + const originalRecipientAddress = payload + .subarray(offset, offset + 32) + .toString("hex"); + offset += 32; + const transactionId = payload.subarray(offset, offset + 32).toString("hex"); + offset += 32; + const vout = payload.readUInt32BE(offset); + offset += 4; + inputs.push({ originalRecipientAddress, transactionId, vout }); + } + + // len_output (uint32 BE) + const lenOutput = payload.readUInt32BE(offset); + offset += 4; + + // outputs + const outputs: UnlockPayloadOutput[] = []; + for (let i = 0; i < lenOutput; i++) { + const amount = payload.readBigUInt64BE(offset); + offset += 8; + const addressType = payload.readUInt32BE(offset); + offset += 4; + // Address length determined by address_type: 20 for P2PKH/P2SH, 32 for P2WSH/P2TR + const addrLen = addressType <= 1 ? 20 : 32; + const address = payload.subarray(offset, offset + addrLen).toString("hex"); + offset += addrLen; + outputs.push({ amount, addressType, address }); + } + + return { destinationChain, delegatedManagerSet, inputs, outputs }; +} + +// Parse VAA structure +export function parseVAA(vaaBytes: Buffer): ParsedVAA { + let offset = 0; + + // Version (1 byte) + const version = vaaBytes.readUInt8(offset); + offset += 1; + + // Guardian set index (4 bytes BE) + const guardianSetIndex = vaaBytes.readUInt32BE(offset); + offset += 4; + + // Number of signatures (1 byte) + const numSignatures = vaaBytes.readUInt8(offset); + offset += 1; + + // Signatures (66 bytes each: 1 byte index + 65 bytes signature) + const signatures: { guardianIndex: number; signature: Buffer }[] = []; + for (let i = 0; i < numSignatures; i++) { + const guardianIndex = vaaBytes.readUInt8(offset); + offset += 1; + const signature = vaaBytes.subarray(offset, offset + 65); + offset += 65; + signatures.push({ guardianIndex, signature: Buffer.from(signature) }); + } + + // Timestamp (4 bytes BE) + const timestamp = vaaBytes.readUInt32BE(offset); + offset += 4; + + // Nonce (4 bytes BE) + const nonce = vaaBytes.readUInt32BE(offset); + offset += 4; + + // Emitter chain (2 bytes BE) + const emitterChain = vaaBytes.readUInt16BE(offset); + offset += 2; + + // Emitter address (32 bytes) + const emitterAddress = vaaBytes.subarray(offset, offset + 32).toString("hex"); + offset += 32; + + // Sequence (8 bytes BE) + const sequence = vaaBytes.readBigUInt64BE(offset); + offset += 8; + + // Consistency level (1 byte) + const consistencyLevel = vaaBytes.readUInt8(offset); + offset += 1; + + // Payload (rest of the data) + const payload = vaaBytes.subarray(offset); + + return { + version, + guardianSetIndex, + signatures, + timestamp, + nonce, + emitterChain, + emitterAddress, + sequence, + consistencyLevel, + payload: Buffer.from(payload), + }; +} diff --git a/testing/dogecoin/withdraw-testnet-executor.ts b/testing/dogecoin/withdraw-testnet-executor.ts new file mode 100644 index 0000000000..64516e70fa --- /dev/null +++ b/testing/dogecoin/withdraw-testnet-executor.ts @@ -0,0 +1,482 @@ +import { AnchorProvider, BN, Program, Wallet } from "@anchor-lang/core"; +import { + Connection, + Keypair, + PublicKey, + sendAndConfirmTransaction, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + Transaction, +} from "@solana/web3.js"; +import * as bitcoin from "bitcoinjs-lib"; +import type { WormholePostMessageShim } from "../../svm/wormhole-core-shims/anchor/idls/wormhole_post_message_shim"; +import postMessageShimIdl from "../../svm/wormhole-core-shims/anchor/idls/wormhole_post_message_shim.json"; +import type { Executor } from "./idls/executor"; +import executorIdl from "./idls/executor.json"; +import { FEE, fetchUtxos, KOINU_PER_DOGE } from "./shared"; +import { ADDRESS_TYPE_P2PKH, encodeUnlockPayload } from "./vaa"; +import { deserialize, serialize } from "binary-layout"; +import { toBytes } from "viem"; +import { + RequestPrefix, + signedQuoteLayout, +} from "@wormhole-foundation/sdk-definitions"; +import { requestLayout } from "./requestForExecution"; + +// Wormhole constants +const WORMHOLE_CHAIN_ID_SOLANA = 1; +const WORMHOLE_CHAIN_ID_DOGECOIN = 65; +const SOLANA_DEVNET_RPC = "https://api.devnet.solana.com"; + +// Program addresses +const WORMHOLE_CORE_BRIDGE_DEVNET = new PublicKey( + "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5", +); +const POST_MESSAGE_SHIM = new PublicKey( + "EtZMZM22ViKMo4r5y4Anovs3wKQ2owUmDpjygnMMcdEX", +); + +// Executor API +const EXECUTOR_API = "https://executor-testnet.labsapis.com"; + +// Delegated manager set index +const DELEGATED_MANAGER_SET = 1; + +// Load Solana keypair +async function loadSolanaKeypair(): Promise { + const keypairJson = await Bun.file("solana-devnet-keypair.json").json(); + return Keypair.fromSecretKey(new Uint8Array(keypairJson.privateKeyArray)); +} + +// Load Solana keypair +const solanaKeypair = await loadSolanaKeypair(); +console.log("Solana address:", solanaKeypair.publicKey.toBase58()); + +const connection = new Connection(SOLANA_DEVNET_RPC, "confirmed"); + +const provider = new AnchorProvider(connection, new Wallet(solanaKeypair)); + +// Create Anchor programs +const postMessageShimProgram = new Program( + postMessageShimIdl as WormholePostMessageShim, + provider, +); +const executorProgram = new Program( + executorIdl as Executor, + provider, +); + +// Derive PDAs for the post message shim +function deriveShimAccounts(emitter: PublicKey) { + const [bridge] = PublicKey.findProgramAddressSync( + [Buffer.from("Bridge")], + WORMHOLE_CORE_BRIDGE_DEVNET, + ); + + const [message] = PublicKey.findProgramAddressSync( + [emitter.toBuffer()], + POST_MESSAGE_SHIM, + ); + + const [sequence] = PublicKey.findProgramAddressSync( + [Buffer.from("Sequence"), emitter.toBuffer()], + WORMHOLE_CORE_BRIDGE_DEVNET, + ); + + const [feeCollector] = PublicKey.findProgramAddressSync( + [Buffer.from("fee_collector")], + WORMHOLE_CORE_BRIDGE_DEVNET, + ); + + const [eventAuthority] = PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + POST_MESSAGE_SHIM, + ); + + return { bridge, message, sequence, feeCollector, eventAuthority }; +} + +// Build post message instruction using Anchor +function buildPostMessageInstruction( + payer: PublicKey, + emitter: PublicKey, + nonce: number, + payload: Buffer, + consistencyLevel: { confirmed: {} } | { finalized: {} } = { confirmed: {} }, +) { + const accounts = deriveShimAccounts(emitter); + + return postMessageShimProgram.methods + .postMessage(nonce, consistencyLevel, Buffer.from(payload)) + .accountsPartial({ + bridge: accounts.bridge, + message: accounts.message, + emitter, + sequence: accounts.sequence, + payer, + feeCollector: accounts.feeCollector, + clock: SYSVAR_CLOCK_PUBKEY, + systemProgram: SystemProgram.programId, + wormholeProgram: WORMHOLE_CORE_BRIDGE_DEVNET, + eventAuthority: accounts.eventAuthority, + program: POST_MESSAGE_SHIM, + }) + .instruction(); +} + +// ============================================================================ +// Executor API Types and Functions +// ============================================================================ + +interface ExecutorQuote { + signedQuote: string; + estimatedCost: string; +} + +interface ExecutorStatusTx { + txHash: string; + chainId: number; + blockNumber: string; + blockTime: string; + cost: string; +} + +interface ExecutorStatus { + chainId: number; + estimatedCost: string; + id: string; + indexedAt: string; + status: string; + txHash: string; + txs: ExecutorStatusTx[]; +} + +// Fetch execution status from executor API +async function fetchExecutorStatus( + txHash: string, + chainId: number, +): Promise { + const url = `${EXECUTOR_API}/v0/status/tx`; + + const response = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + txHash, + chainId, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to fetch executor status: ${error}`); + } + + return response.json(); +} + +// Poll executor status until completion or timeout +async function pollExecutorStatus( + txHash: string, + chainId: number, + maxAttempts: number = 30, + intervalMs: number = 10000, +): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + console.log(` Polling status (attempt ${attempt}/${maxAttempts})...`); + + const statuses = await fetchExecutorStatus(txHash, chainId); + + if (statuses.length > 0 && statuses[0]) { + const status = statuses[0]; + console.log(` Status: ${status.status}`); + + if (status.txs && status.txs.length > 0) { + console.log(` Destination transactions:`); + for (const tx of status.txs) { + console.log(` Chain ${tx.chainId}: ${tx.txHash}`); + console.log( + ` Block: ${tx.blockNumber}, Time: ${tx.blockTime}`, + ); + } + } + + // Check if execution is complete + if (status.status === "submitted" || status.status === "completed") { + if (status.txs && status.txs.length > 0) { + console.log(`\n Execution complete!`); + return statuses; + } + } + + // Check for failure states + if (status.status === "failed" || status.status === "expired") { + console.log(`\n Execution ${status.status}.`); + return statuses; + } + } else { + console.log(` No status found yet...`); + } + + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + } + + console.log(` Max polling attempts reached.`); + return fetchExecutorStatus(txHash, chainId); +} + +// Fetch quote from executor API +async function fetchExecutorQuote( + srcChain: number, + dstChain: number, + relayInstructions: string = "0x", +): Promise { + const url = `${EXECUTOR_API}/v0/quote`; + console.log(`Fetching executor quote from ${url}...`); + + const response = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + srcChain, + dstChain, + relayInstructions, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to fetch executor quote: ${error}`); + } + + return response.json(); +} + +// Build request_for_execution instruction using Anchor +function buildRequestForExecutionInstruction( + payer: PublicKey, + payee: PublicKey, + amount: bigint, + dstChain: number, + refundAddr: PublicKey, + signedQuoteBytes: Buffer, + requestBytes: Buffer, + relayInstructions: Buffer = Buffer.alloc(0), +) { + // dst_addr ([u8; 32]) - not used for Dogecoin, leave as zeros + const dstAddr = Array.from(Buffer.alloc(32)); + + return executorProgram.methods + .requestForExecution({ + amount: new BN(amount.toString()), + dstChain, + dstAddr, + refundAddr, + signedQuoteBytes, + requestBytes, + relayInstructions, + }) + .accountsPartial({ + payer, + payee, + systemProgram: SystemProgram.programId, + }) + .instruction(); +} + +// ============================================================================ +// Main Script +// ============================================================================ + +console.log("=== Wormhole TESTNET Withdraw via Executor ===\n"); + +// Read deposit info +let depositInfo: { + txid: string; + amount: number; + p2shAddress: string; + redeemScript: string; + senderAddress: string; + timestamp: string; +}; + +try { + depositInfo = await Bun.file("deposit-info.json").json(); + console.log("\nLoaded deposit info from deposit-info.json"); + console.log(" Deposit TXID:", depositInfo.txid); + console.log(" Amount:", depositInfo.amount / KOINU_PER_DOGE, "DOGE"); +} catch { + console.error("Error: deposit-info.json not found. Run deposit.ts first."); + process.exit(1); +} + +// Get the recipient address (Solana pubkey as 32-byte hex) +const recipientAddress = solanaKeypair.publicKey.toBuffer().toString("hex"); + +// Fetch UTXOs to determine inputs +console.log("\nFetching UTXOs from P2SH address..."); +const utxos = await fetchUtxos(depositInfo.p2shAddress); +console.log(`Found ${utxos.length} UTXOs`); + +if (utxos.length === 0) { + console.error("No UTXOs available in P2SH address."); + process.exit(1); +} + +// Calculate total available +const totalAvailable = utxos.reduce( + (sum: number, utxo: any) => sum + utxo.value, + 0, +); +console.log(`Total available: ${totalAvailable / KOINU_PER_DOGE} DOGE`); + +const amountToWithdraw = totalAvailable - FEE; +if (amountToWithdraw <= 0) { + console.error("Insufficient funds to cover fee."); + process.exit(1); +} + +// Decode the destination address to get the pubkey hash +const destAddressDecoded = bitcoin.address.fromBase58Check( + depositInfo.senderAddress, +); +const destPubKeyHash = Buffer.from(destAddressDecoded.hash).toString("hex"); + +// Build VAA payload +console.log("\n=== Building VAA Payload ==="); +const payload = encodeUnlockPayload({ + destinationChain: WORMHOLE_CHAIN_ID_DOGECOIN, + delegatedManagerSet: DELEGATED_MANAGER_SET, + inputs: utxos.map((utxo: any) => ({ + originalRecipientAddress: recipientAddress, + transactionId: utxo.txid, + vout: utxo.vout, + })), + outputs: [ + { + amount: BigInt(amountToWithdraw), + addressType: ADDRESS_TYPE_P2PKH, + address: destPubKeyHash, + }, + ], +}); + +console.log("Payload length:", payload.length, "bytes"); +console.log("Payload (hex):", payload.toString("hex")); + +// Fetch executor quote +console.log("\n=== Fetching Executor Quote ==="); +let quote: ExecutorQuote; +let payee: PublicKey; +try { + quote = await fetchExecutorQuote( + WORMHOLE_CHAIN_ID_SOLANA, + WORMHOLE_CHAIN_ID_DOGECOIN, + "0x", // Empty relay instructions for Dogecoin + ); + payee = new PublicKey( + deserialize(signedQuoteLayout, toBytes(quote.signedQuote)).quote + .payeeAddress, + ); +} catch (error) { + console.error("Failed to fetch executor quote:", error); + process.exit(1); +} + +// Build the transaction with both instructions +console.log("\n=== Building Solana Transaction ==="); + +const emitter = solanaKeypair.publicKey; +const accounts = deriveShimAccounts(emitter); + +// Read current sequence to predict the next one +const sequenceAccountInfo = await connection.getAccountInfo(accounts.sequence); +let nextSequence = BigInt(0); +if (sequenceAccountInfo && sequenceAccountInfo.data.length >= 8) { + nextSequence = sequenceAccountInfo.data.readBigUInt64LE(0); +} +console.log("Next sequence:", nextSequence.toString()); + +// Build ERV1 request bytes +const requestBytes = serialize(requestLayout, { + request: { + prefix: RequestPrefix.ERV1, + chain: WORMHOLE_CHAIN_ID_SOLANA, + address: emitter.toBytes(), + sequence: nextSequence, + }, +}); +console.log("Request bytes (ERV1):", requestBytes); + +// Instruction 1: Post Wormhole message +const postMessageIx = await buildPostMessageInstruction( + solanaKeypair.publicKey, + emitter, + Date.now() % 2 ** 32, // Use timestamp as nonce + payload, + { confirmed: {} }, +); + +// Instruction 2: Request for execution +const signedQuoteBytes = Buffer.from( + quote.signedQuote.replace("0x", ""), + "hex", +); + +const requestForExecutionIx = await buildRequestForExecutionInstruction( + solanaKeypair.publicKey, + payee, + BigInt(quote.estimatedCost), + WORMHOLE_CHAIN_ID_DOGECOIN, + solanaKeypair.publicKey, // refund to self + signedQuoteBytes, + Buffer.from(requestBytes), // ERV1 request bytes + Buffer.alloc(0), // Empty relay instructions +); + +// Build transaction with both instructions +const tx = new Transaction().add( + // Fee transfer to Wormhole + SystemProgram.transfer({ + fromPubkey: solanaKeypair.publicKey, + toPubkey: accounts.feeCollector, + lamports: 100, + }), + // Post Wormhole message + postMessageIx, + // Request executor to relay + requestForExecutionIx, +); + +// Send the transaction +console.log("\n=== Submitting Transaction ==="); +let signature: string; +try { + signature = await sendAndConfirmTransaction(connection, tx, [solanaKeypair], { + commitment: "confirmed", + }); + console.log("Transaction confirmed!"); + console.log(" Signature:", signature); +} catch (error) { + console.error("Transaction failed:", error); + process.exit(1); +} + +// Build executor explorer URL +const executorExplorerUrl = `https://wormholelabs-xyz.github.io/executor-explorer/#/chain/${WORMHOLE_CHAIN_ID_SOLANA}/tx/${signature}?endpoint=https%3A%2F%2Fexecutor-testnet.labsapis.com&env=Testnet`; +console.log("\nExecutor Explorer:", executorExplorerUrl); + +await pollExecutorStatus( + signature, + WORMHOLE_CHAIN_ID_SOLANA, + 30, // max attempts + 10000, // 10 second interval +); diff --git a/testing/dogecoin/withdraw-testnet.ts b/testing/dogecoin/withdraw-testnet.ts new file mode 100644 index 0000000000..48bf9ea75d --- /dev/null +++ b/testing/dogecoin/withdraw-testnet.ts @@ -0,0 +1,585 @@ +import { + Connection, + Keypair, + PublicKey, + sendAndConfirmTransaction, + SystemProgram, + SYSVAR_CLOCK_PUBKEY, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import * as bitcoin from "bitcoinjs-lib"; +import { + fetchManagerSignatures, + loadManagerKeys, + type ManagerSignaturesResponse, +} from "./manager"; +import { buildRedeemScript, dogecoinTestnet } from "./redeem-script"; +import { + broadcastTx, + DOGECOIN_CHAIN_ID, + explorerTxUrl, + FEE, + fetchUtxos, + KOINU_PER_DOGE, +} from "./shared"; +import { + ADDRESS_TYPE_P2PKH, + ADDRESS_TYPE_P2SH, + decodeUnlockPayload, + encodeUnlockPayload, +} from "./vaa"; + +// Wormhole constants +const WORMHOLE_CHAIN_ID_SOLANA = 1; +const WORMHOLE_CHAIN_ID_DOGECOIN = 65; +const SOLANA_DEVNET_RPC = "https://api.devnet.solana.com"; +const GUARDIAN_RPC = "http://136.119.196.246/"; + +// Program addresses +const WORMHOLE_CORE_BRIDGE_DEVNET = new PublicKey( + "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5", +); +const POST_MESSAGE_SHIM = new PublicKey( + "EtZMZM22ViKMo4r5y4Anovs3wKQ2owUmDpjygnMMcdEX", +); + +// Delegated manager set index (placeholder) +const DELEGATED_MANAGER_SET = 1; + +// Post message shim instruction discriminator +const POST_MESSAGE_DISCRIMINATOR = Buffer.from([ + 214, 50, 100, 209, 38, 34, 7, 76, +]); + +// Finality enum +const FINALITY_CONFIRMED = 0; +const FINALITY_FINALIZED = 1; + +// Load Solana keypair +async function loadSolanaKeypair(): Promise { + const keypairJson = await Bun.file("solana-devnet-keypair.json").json(); + return Keypair.fromSecretKey(new Uint8Array(keypairJson.privateKeyArray)); +} + +// Derive PDAs for the post message shim +function deriveShimAccounts(emitter: PublicKey) { + // Bridge config PDA (from Wormhole Core Bridge) + const [bridge] = PublicKey.findProgramAddressSync( + [Buffer.from("Bridge")], + WORMHOLE_CORE_BRIDGE_DEVNET, + ); + + // Message PDA (from Post Message Shim, seeded by emitter) + const [message] = PublicKey.findProgramAddressSync( + [emitter.toBuffer()], + POST_MESSAGE_SHIM, + ); + + // Sequence PDA (from Wormhole Core Bridge) + const [sequence] = PublicKey.findProgramAddressSync( + [Buffer.from("Sequence"), emitter.toBuffer()], + WORMHOLE_CORE_BRIDGE_DEVNET, + ); + + // Fee collector PDA (from Wormhole Core Bridge) + const [feeCollector] = PublicKey.findProgramAddressSync( + [Buffer.from("fee_collector")], + WORMHOLE_CORE_BRIDGE_DEVNET, + ); + + // Event authority PDA (from Post Message Shim) + const [eventAuthority] = PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + POST_MESSAGE_SHIM, + ); + + return { bridge, message, sequence, feeCollector, eventAuthority }; +} + +// Build post message instruction +function buildPostMessageInstruction( + payer: PublicKey, + emitter: PublicKey, + nonce: number, + payload: Buffer, + finality: number = FINALITY_FINALIZED, +): TransactionInstruction { + const accounts = deriveShimAccounts(emitter); + + // Build instruction data: discriminator + nonce (u32 LE) + finality (u8) + payload (borsh bytes) + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonce); + + // Borsh-encode the payload (4-byte length prefix LE + data) + const payloadLenBuffer = Buffer.alloc(4); + payloadLenBuffer.writeUInt32LE(payload.length); + + const instructionData = Buffer.concat([ + new Uint8Array(POST_MESSAGE_DISCRIMINATOR), + new Uint8Array(nonceBuffer), + new Uint8Array([finality]), // Finality enum as u8 + new Uint8Array(payloadLenBuffer), + new Uint8Array(payload), + ]); + + return new TransactionInstruction({ + keys: [ + { pubkey: accounts.bridge, isSigner: false, isWritable: true }, + { pubkey: accounts.message, isSigner: false, isWritable: true }, + { pubkey: emitter, isSigner: true, isWritable: false }, + { pubkey: accounts.sequence, isSigner: false, isWritable: true }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: accounts.feeCollector, isSigner: false, isWritable: true }, + { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { + pubkey: WORMHOLE_CORE_BRIDGE_DEVNET, + isSigner: false, + isWritable: false, + }, + { pubkey: accounts.eventAuthority, isSigner: false, isWritable: false }, + { pubkey: POST_MESSAGE_SHIM, isSigner: false, isWritable: false }, + ], + programId: POST_MESSAGE_SHIM, + data: instructionData, + }); +} + +// Post message using the shim +async function postWormholeMessage( + connection: Connection, + payer: Keypair, + payload: Buffer, + nonce: number = 0, +): Promise<{ sequence: bigint; emitterAddress: string; signature: string }> { + const emitter = payer.publicKey; + const accounts = deriveShimAccounts(emitter); + + const instruction = buildPostMessageInstruction( + payer.publicKey, + emitter, + nonce, + payload, + FINALITY_CONFIRMED, + ); + + const tx = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: accounts.feeCollector, + lamports: 100, // TODO: dynamically read bridge config + }), + instruction, + ); + + console.log("Posting Wormhole message via shim..."); + console.log(" Emitter:", emitter.toBase58()); + console.log(" Message PDA:", accounts.message.toBase58()); + console.log(" Sequence PDA:", accounts.sequence.toBase58()); + + const signature = await sendAndConfirmTransaction(connection, tx, [payer], { + commitment: "confirmed", + }); + console.log("Transaction signature:", signature); + + // TODO: read the sequence from the shim log + // Fetch sequence from the sequence account + const sequenceAccountInfo = await connection.getAccountInfo( + accounts.sequence, + ); + let sequence = BigInt(0); + if (sequenceAccountInfo && sequenceAccountInfo.data.length >= 8) { + // Sequence is stored as u64 LE at offset 0 + sequence = sequenceAccountInfo.data.readBigUInt64LE(0) - 1n; + } + + // Emitter address as 32-byte hex (left-padded) + const emitterAddress = emitter.toBuffer().toString("hex"); + + return { sequence, emitterAddress, signature }; +} + +// ============================================================================ +// Main Script +// ============================================================================ + +console.log("=== Wormhole TESTNET Withdraw ===\n"); + +// Load Solana keypair +const solanaKeypair = await loadSolanaKeypair(); +console.log("Solana address:", solanaKeypair.publicKey.toBase58()); + +// Load manager keys +const { + mThreshold, + nTotal, + pubkeys: managerPubkeys, +} = await loadManagerKeys(DOGECOIN_CHAIN_ID); + +console.log("\nManager keys:"); +for (let i = 0; i < managerPubkeys.length; i++) { + console.log(` ${i}: ${managerPubkeys[i]!.toString("hex")}`); +} + +// Read deposit info +let depositInfo: { + txid: string; + amount: number; + p2shAddress: string; + redeemScript: string; + senderAddress: string; + timestamp: string; +}; + +try { + depositInfo = await Bun.file("deposit-info.json").json(); + console.log("\nLoaded deposit info from deposit-info.json"); + console.log(" Deposit TXID:", depositInfo.txid); + console.log(" Amount:", depositInfo.amount / KOINU_PER_DOGE, "DOGE"); +} catch { + console.error("Error: deposit-info.json not found. Run deposit.ts first."); + process.exit(1); +} + +// Get the recipient address (Solana pubkey as 32-byte hex) +const recipientAddress = solanaKeypair.publicKey.toBuffer().toString("hex"); + +// Fetch UTXOs to determine inputs +console.log("\nFetching UTXOs from P2SH address..."); +const utxos = await fetchUtxos(depositInfo.p2shAddress); +console.log(`Found ${utxos.length} UTXOs`); + +if (utxos.length === 0) { + console.error("No UTXOs available in P2SH address."); + process.exit(1); +} + +// Calculate total available +const totalAvailable = utxos.reduce( + (sum: number, utxo: any) => sum + utxo.value, + 0, +); +console.log(`Total available: ${totalAvailable / KOINU_PER_DOGE} DOGE`); + +const amountToWithdraw = totalAvailable - FEE; +if (amountToWithdraw <= 0) { + console.error("Insufficient funds to cover fee."); + process.exit(1); +} + +// Decode the destination address to get the pubkey hash +const destAddressDecoded = bitcoin.address.fromBase58Check( + depositInfo.senderAddress, +); +const destPubKeyHash = Buffer.from(destAddressDecoded.hash).toString("hex"); + +// Build VAA payload +console.log("\n=== Building VAA Payload ==="); +const payload = encodeUnlockPayload({ + destinationChain: WORMHOLE_CHAIN_ID_DOGECOIN, + delegatedManagerSet: DELEGATED_MANAGER_SET, + inputs: utxos.map((utxo: any) => ({ + originalRecipientAddress: recipientAddress, + transactionId: utxo.txid, + vout: utxo.vout, + })), + outputs: [ + { + amount: BigInt(amountToWithdraw), + addressType: ADDRESS_TYPE_P2PKH, + address: destPubKeyHash, + }, + ], +}); + +console.log("Payload length:", payload.length, "bytes"); +console.log("Payload (hex):", payload.toString("hex")); + +// Post message to Wormhole via shim +const connection = new Connection(SOLANA_DEVNET_RPC, "confirmed"); +console.log("\n=== Posting Wormhole Message via Shim ==="); + +let sequence: bigint; +let emitterAddress: string; +let signature: string; +try { + const result = await postWormholeMessage( + connection, + solanaKeypair, + payload, + Date.now() % 2 ** 32, // Use timestamp as nonce + ); + sequence = result.sequence; + emitterAddress = result.emitterAddress; + signature = result.signature; + console.log("\nMessage posted successfully!"); + console.log(" Emitter:", emitterAddress); + console.log(" Sequence:", sequence.toString()); +} catch (error) { + console.error("Failed to post Wormhole message:", error); + process.exit(1); +} + +// Fetch signatures from Guardian Manager +console.log("\n=== Fetching Signatures from Guardian Manager ==="); +let managerSignatures: ManagerSignaturesResponse | null = null; + +try { + managerSignatures = await fetchManagerSignatures( + GUARDIAN_RPC, + WORMHOLE_CHAIN_ID_SOLANA, + emitterAddress, + sequence, + 30, // max retries + 2000, // retry delay (2 seconds) + ); + + console.log("\nManager Signatures Response:"); + console.log(" VAA Hash:", managerSignatures.vaaHash); + console.log(" VAA ID:", managerSignatures.vaaId); + console.log(" Destination Chain:", managerSignatures.destinationChain); + console.log(" Manager Set Index:", managerSignatures.managerSetIndex); + console.log(" Required:", managerSignatures.required); + console.log(" Total:", managerSignatures.total); + console.log(" Signatures:"); + for (const sig of managerSignatures.signatures) { + console.log( + ` Signer ${sig.signerIndex}: ${sig.signatures[0]?.slice(0, 20)}...`, + ); + } +} catch (error) { + console.error("Failed to fetch manager signatures:", error); + console.log("\nContinuing without signatures for testing..."); +} + +// Use the original payload we built (we don't need to parse it from VAA) +const vaaPayload = payload; + +// Parse the VAA payload +console.log("\n=== Parsing VAA Payload ==="); +const decodedPayload = decodeUnlockPayload(vaaPayload); +console.log("Destination Chain:", decodedPayload.destinationChain); +console.log("Delegated Manager Set:", decodedPayload.delegatedManagerSet); +console.log("Inputs:", decodedPayload.inputs.length); +for (const input of decodedPayload.inputs) { + console.log(" - Recipient:", input.originalRecipientAddress); + console.log(" TxID:", input.transactionId); + console.log(" Vout:", input.vout); +} +console.log("Outputs:", decodedPayload.outputs.length); +for (const output of decodedPayload.outputs) { + console.log(" - Amount:", output.amount.toString(), "koinu"); + console.log(" Type:", output.addressType === 0 ? "P2PKH" : "P2SH"); + console.log(" Address:", output.address); +} + +// Build redeem script from VAA data +console.log("\n=== Building Redeem Script from VAA ==="); +const vaaRecipientAddress = decodedPayload.inputs[0]?.originalRecipientAddress; +if (!vaaRecipientAddress) { + console.error("No inputs in VAA payload"); + process.exit(1); +} + +const redeemScript = buildRedeemScript({ + emitterChain: WORMHOLE_CHAIN_ID_SOLANA, + emitterContract: vaaRecipientAddress, + recipientAddress: vaaRecipientAddress, + managerPubkeys, + mThreshold, + nTotal, +}); + +console.log("Redeem script length:", redeemScript.length, "bytes"); +console.log("Redeem script (hex):", redeemScript.toString("hex")); + +// Note: The redeem script won't match because deposit used different emitter chain +// For devnet testing, we use the deposit's redeem script +console.log("\nNote: Using deposit redeem script for signing..."); +const depositRedeemScript = Buffer.from(depositInfo.redeemScript, "hex"); + +// Generate P2SH address from redeem script +const p2sh = bitcoin.payments.p2sh({ + redeem: { output: new Uint8Array(depositRedeemScript) }, + network: dogecoinTestnet, +}); + +console.log("P2SH Address:", p2sh.address); + +// Build the transaction from VAA data +console.log("\n=== Building Dogecoin Transaction ==="); +const tx = new bitcoin.Transaction(); +tx.version = 1; + +// Add inputs from VAA +for (const input of decodedPayload.inputs) { + tx.addInput( + new Uint8Array(Buffer.from(input.transactionId, "hex").reverse()), + input.vout, + ); +} + +// Add outputs from VAA +for (const output of decodedPayload.outputs) { + let outputScript: Uint8Array; + if (output.addressType === ADDRESS_TYPE_P2PKH) { + // P2PKH: OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG + outputScript = bitcoin.script.compile([ + bitcoin.opcodes.OP_DUP, + bitcoin.opcodes.OP_HASH160, + new Uint8Array(Buffer.from(output.address, "hex")), + bitcoin.opcodes.OP_EQUALVERIFY, + bitcoin.opcodes.OP_CHECKSIG, + ]); + } else if (output.addressType === ADDRESS_TYPE_P2SH) { + // P2SH: OP_HASH160 <20-byte hash> OP_EQUAL + outputScript = bitcoin.script.compile([ + bitcoin.opcodes.OP_HASH160, + new Uint8Array(Buffer.from(output.address, "hex")), + bitcoin.opcodes.OP_EQUAL, + ]); + } else { + throw new Error(`Unknown address type: ${output.addressType}`); + } + tx.addOutput(outputScript, output.amount); +} + +console.log("\nTransaction built (unsigned)"); +console.log(" Inputs:", tx.ins.length); +console.log(" Outputs:", tx.outs.length); + +// Sign with manager signatures +if (managerSignatures && managerSignatures.isComplete) { + console.log( + `\n=== Signing with ${managerSignatures.required} of ${managerSignatures.total} manager signatures ===`, + ); + + // Sort signatures by signer index to match the order in the redeem script + const sortedSigs = [...managerSignatures.signatures].sort( + (a, b) => a.signerIndex - b.signerIndex, + ); + + // Take only the required number of signatures + const requiredSigs = sortedSigs.slice(0, managerSignatures.required); + + console.log( + "Using signatures from signers:", + requiredSigs.map((s) => s.signerIndex).join(", "), + ); + + // Build the scriptSig for each input + for ( + let inputIndex = 0; + inputIndex < decodedPayload.inputs.length; + inputIndex++ + ) { + // Decode base64 signatures and convert to Uint8Array + const signatures: Uint8Array[] = requiredSigs.map((sig) => { + // Each signer may have multiple signatures (one per input), use the one for this input + const sigBase64 = sig.signatures[inputIndex] ?? sig.signatures[0]; + if (!sigBase64) { + throw new Error( + `No signature found for signer ${sig.signerIndex} input ${inputIndex}`, + ); + } + return new Uint8Array(Buffer.from(sigBase64, "base64")); + }); + + const scriptSig = bitcoin.script.compile([ + bitcoin.opcodes.OP_0, // Required for CHECKMULTISIG bug + ...signatures, + new Uint8Array(depositRedeemScript), + ]); + + tx.setInputScript(inputIndex, scriptSig); + console.log( + ` Input ${inputIndex}: Applied ${signatures.length} signatures`, + ); + } + + // Serialize the transaction + const txHex = tx.toHex(); + + console.log("\nTransaction hex:", txHex); + console.log("Transaction ID:", tx.getId()); + + // Broadcast the transaction + console.log("\nBroadcasting transaction..."); + try { + const txid = await broadcastTx(txHex); + console.log("Transaction broadcast successfully!"); + console.log("TXID:", txid); + console.log(`\nView on explorer: ${explorerTxUrl(txid)}`); + + // Save withdraw info + const withdrawInfo = { + solTxHash: signature, + sequence: sequence.toString(), + emitterAddress, + emitterChain: WORMHOLE_CHAIN_ID_SOLANA, + payload: payload.toString("hex"), + dogeTxId: txid, + amount: Number(decodedPayload.outputs[0]?.amount ?? 0), + destinationAddress: depositInfo.senderAddress, + p2shAddress: p2sh.address, + managerSetIndex: managerSignatures.managerSetIndex, + signersUsed: requiredSigs.map((s) => s.signerIndex), + timestamp: new Date().toISOString(), + }; + await Bun.write( + "withdraw-devnet-info.json", + JSON.stringify(withdrawInfo, null, 2), + ); + console.log("\nWithdraw info saved to withdraw-devnet-info.json"); + } catch (error) { + console.error("Broadcast failed:", error); + + // Save failed withdraw info for debugging + const withdrawInfo = { + solTxHash: signature, + sequence: sequence.toString(), + emitterAddress, + emitterChain: WORMHOLE_CHAIN_ID_SOLANA, + payload: payload.toString("hex"), + dogeTxHex: txHex, + dogeTxId: tx.getId(), + amount: Number(decodedPayload.outputs[0]?.amount ?? 0), + destinationAddress: depositInfo.senderAddress, + p2shAddress: p2sh.address, + managerSetIndex: managerSignatures.managerSetIndex, + signersUsed: requiredSigs.map((s) => s.signerIndex), + error: String(error), + timestamp: new Date().toISOString(), + }; + await Bun.write( + "withdraw-devnet-info.json", + JSON.stringify(withdrawInfo, null, 2), + ); + console.log("\nFailed withdraw info saved to withdraw-devnet-info.json"); + } +} else { + console.log("\n=== No Manager Signatures Available ==="); + console.log("Cannot sign transaction without manager signatures."); + console.log("Transaction ID (unsigned):", tx.getId()); + + // Save unsigned withdraw info + const withdrawInfo = { + solTxHash: signature, + sequence: sequence.toString(), + emitterAddress, + emitterChain: WORMHOLE_CHAIN_ID_SOLANA, + payload: payload.toString("hex"), + dogeTxId: tx.getId(), + amount: Number(decodedPayload.outputs[0]?.amount ?? 0), + destinationAddress: depositInfo.senderAddress, + p2shAddress: p2sh.address, + signed: false, + timestamp: new Date().toISOString(), + }; + await Bun.write( + "withdraw-devnet-info.json", + JSON.stringify(withdrawInfo, null, 2), + ); + console.log("\nUnsigned withdraw info saved to withdraw-devnet-info.json"); +}