diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2bc0c9a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ci: + name: Check & Test + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Install Solana CLI + run: | + sh -c "$(curl -sSfL https://release.anza.xyz/v2.3.0/install)" + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + + - run: cargo install --git https://github.com/coral-xyz/anchor avm --force + - run: avm install 0.32.1 && avm use 0.32.1 + + - run: npm ci + - run: anchor build + - run: npm run check + + - name: Generate keypair + run: solana-keygen new --no-bip39-passphrase --force + + - run: anchor test --skip-build diff --git a/README.md b/README.md index 29c7a75..a340ec3 100644 --- a/README.md +++ b/README.md @@ -9,37 +9,51 @@ Built with [Anchor](https://www.anchor-lang.com/) 0.32. ## How it works +### Direct signing (local ETH key) + +1. **Build** inner instructions off-chain (SPL transfers, swaps, etc.) +2. **Compute** the message hash (see [below](#message-hash)) +3. **Sign** the hash with an ETH private key → `(signature, recovery_id)` +4. **Submit** the Solana tx with the signature + indexed inner instructions +5. **On-chain** — the program verifies the ECDSA signature, recovers the ETH address, asserts the nonce, and executes each inner instruction as a CPI from the PDA + +### MPC signing (via [Signet](https://docs.sig.network) network) + +Instead of a local private key, an EVM wallet requests a distributed signature from the [Signet MPC network](https://docs.sig.network). The on-chain program does not distinguish between local-key and MPC signatures — verification is identical. + +1. **Derive** an ETH address from the MPC network using a derivation path +2. **Initialize** the wallet PDA on Solana with that derived address +3. **Build** inner instructions + compute the message hash (same as direct signing) +4. **Request** a signature via `createSignatureRequest(hash, path)` on the Signet `ChainSignatureContract` (Sepolia) — the MPC network produces `(r, s, v)` +5. **Submit** the signature to the Solana program +6. **On-chain** — identical verification: ECDSA recovery, address comparison, nonce check, CPI dispatch + +See the [Signet docs](https://docs.sig.network) for more on chain signatures and MPC key derivation. The MPC test (`tests/mpc-ecdsa-proxy.ts`) requires Sepolia and Solana devnet credentials via environment variables. + +### Message hash + +Both paths produce the same hash structure: + ``` -┌──────────────────────────────────────────────────────────────────────────┐ -│ Off-chain (Client) │ -│ │ -│ 1. Build inner instructions (SPL transfers, swaps, etc.) │ -│ 2. Compute message hash: │ -│ keccak256( │ -│ "\x19Ethereum Signed Message:\n32" || │ -│ keccak256(chain_id || program_id || nonce || │ -│ keccak256(remaining_accounts) || │ -│ keccak256(borsh(inner_instructions))) │ -│ ) │ -│ 3. Sign with ETH private key → (signature, recovery_id) │ -│ 4. Submit Solana tx with signature + indexed inner instructions │ -└─────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ On-chain (Solana Program) │ -│ │ -│ 1. Assert nonce == wallet_state.nonce │ -│ 2. Reject high-S signatures (malleability) │ -│ 3. Recompute message hash from tx data │ -│ 4. secp256k1_recover(hash, signature, recovery_id) → pubkey │ -│ 5. keccak256(pubkey)[12..] → recovered ETH address │ -│ 6. Assert recovered address == wallet_state.eth_address │ -│ 7. Increment nonce │ -│ 8. invoke_signed() each inner instruction with PDA as signer │ -└──────────────────────────────────────────────────────────────────────────┘ +keccak256( + "\x19Ethereum Signed Message:\n32" || + keccak256(chain_id || program_id || nonce || + keccak256(remaining_accounts) || + keccak256(borsh(inner_instructions))) +) ``` +### On-chain verification + +1. Assert `nonce == wallet_state.nonce` +2. Reject high-S signatures (malleability) +3. Recompute message hash from tx data +4. `secp256k1_recover(hash, signature, recovery_id)` → pubkey +5. `keccak256(pubkey)[12..]` → recovered ETH address +6. Assert recovered address == `wallet_state.eth_address` +7. Increment nonce +8. `invoke_signed()` each inner instruction with PDA as signer + ## Instructions | Instruction | Discriminator | Description | @@ -98,8 +112,11 @@ programs/ecdsa-proxy/src/ └── close_wallet.rs tests/ -├── ecdsa-proxy.ts # Integration tests -└── helpers/evm-signer.ts # TypeScript signing (mirrors on-chain hashing) +├── ecdsa-proxy.ts # Integration tests (local key) +├── mpc-ecdsa-proxy.ts # Integration tests (MPC signing via Sepolia) +└── helpers/ + ├── evm-signer.ts # TypeScript signing (mirrors on-chain hashing) + └── mpc-signer.ts # MPC signer using signet.js ChainSignatureContract ``` ## License diff --git a/package-lock.json b/package-lock.json index a6799a2..5847ea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,59 +21,13 @@ "knip": "^5.86.0", "mocha": "^11.7.5", "prettier": "^3.8.1", - "signet.js": "^0.3.1", + "signet.js": "^0.4.0", "ts-mocha": "^11.1.0", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "viem": "^2.47.4" } }, - "../signet.js": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@coral-xyz/anchor": "^0.31.0", - "@cosmjs/amino": "^0.32.4", - "@cosmjs/crypto": "^0.32.4", - "@cosmjs/encoding": "^0.32.4", - "@cosmjs/math": "^0.32.4", - "@cosmjs/proto-signing": "^0.32.4", - "@cosmjs/stargate": "^0.32.4", - "@scure/base": "^1.2.4", - "@solana/web3.js": "^1.98.0", - "bech32": "^2.0.0", - "bitcoinjs-lib": "^6.1.5", - "bn.js": "^5.2.1", - "chain-registry": "^1.69.72", - "coinselect": "^3.1.13", - "cosmjs-types": "^0.9.0", - "elliptic": "^6.6.1", - "viem": "^2.22.14" - }, - "devDependencies": { - "@eslint/js": "^9.18.0", - "@noble/curves": "^1.8.1", - "@nomicfoundation/hardhat-toolbox": "^5.0.0", - "@types/bn.js": "^5.1.5", - "@types/elliptic": "^6.4.18", - "@types/node": "^22.10.10", - "@types/react": "^19.0.0", - "dotenv": "^16.4.5", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.0", - "eslint-plugin-import-x": "^4.6.0", - "hardhat": "^2.22.18", - "prettier": "^3.8.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tsdown": "^0.16.6", - "typescript": "^5.9.3", - "typescript-eslint": "^8.21.0", - "vitest": "^3.0.4", - "vocs": "^1.4.1" - } - }, "node_modules/@adraffy/ens-normalize": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", @@ -90,6 +44,13 @@ "node": ">=6.9.0" } }, + "node_modules/@chain-registry/types": { + "version": "2.0.169", + "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-2.0.169.tgz", + "integrity": "sha512-v0+mDWktDpzBRYQQYcl/fcFcoIJHmFImz/2l/UVJj9NibuX08M1hhYAu44H/nBieDpCdvTgW7Q+bVwZmWlR4gQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@coral-xyz/anchor": { "version": "0.32.1", "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.32.1.tgz", @@ -139,6 +100,156 @@ "@solana/web3.js": "^1.69.0" } }, + "node_modules/@cosmjs/amino": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.38.1.tgz", + "integrity": "sha512-WaThDpq2JwUyKuazq08Xa+FHzQ3jh1HcYnGL4xsyfqFwOlAvnl0EDvSSz9WSwz1oopIxFE9Qtf3OUKOlxBZbYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.38.1", + "@cosmjs/encoding": "^0.38.1", + "@cosmjs/math": "^0.38.1", + "@cosmjs/utils": "^0.38.1" + } + }, + "node_modules/@cosmjs/crypto": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.38.1.tgz", + "integrity": "sha512-r1KQCjKAdMga2aZ/nkgULRF4fisPZMF6ErucVsMmkASBgDl0k9/vD9K9fHUdGClMv0oOYEfwOT/UTBR7K2OuYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/encoding": "^0.38.1", + "@cosmjs/math": "^0.38.1", + "@cosmjs/utils": "^0.38.1", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.2", + "@noble/hashes": "^1.8.0", + "@scure/bip39": "^1.6.0", + "hash-wasm": "^4.12.0" + } + }, + "node_modules/@cosmjs/encoding": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.38.1.tgz", + "integrity": "sha512-i5jGgJhiXs7boePGA48xzYxnCbf3MS5nT/R2HvnvSQyVAG2A99NyL96lBgmz6etOJSGOiwVrB8uh1Qp5bq6WKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^2.0.0", + "base64-js": "^1.3.0", + "readonly-date-esm": "^2.0.0" + } + }, + "node_modules/@cosmjs/encoding/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cosmjs/json-rpc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/json-rpc/-/json-rpc-0.38.1.tgz", + "integrity": "sha512-h+ejJeh+Men8upheRcIETGLAGhsGzIQW03BFvCsCenQOoewpxnbxN1mjfLE7M8UtOTNUsb0uQ1yEnhaTKPTRPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/stream": "^0.38.1", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/math": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.38.1.tgz", + "integrity": "sha512-MBk7p6kPNULi0TusD8O3xoBskFIkRzOtpmnea3sXbTVnguX7epNPVDITXM4tlsg8kAQrEOEIA0g5zAJxzH3Ikw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@cosmjs/proto-signing": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.38.1.tgz", + "integrity": "sha512-J7jELTwk39wYAZUan48eTLBKzx3/max1NIQSlaerdrUkIqhk9ZzC2+p94RnlFUBkvsWZ4ORbhNLYPjwuf2oM0g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/amino": "^0.38.1", + "@cosmjs/crypto": "^0.38.1", + "@cosmjs/encoding": "^0.38.1", + "@cosmjs/math": "^0.38.1", + "@cosmjs/utils": "^0.38.1", + "cosmjs-types": "^0.11.0" + } + }, + "node_modules/@cosmjs/socket": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.38.1.tgz", + "integrity": "sha512-r58RplrHOoO9oDCPiFZu+oBTgeTqE6/HMW+JSAko2Dyv3AocdPhBV16SJQriumGxVRmFUdcczkK2L+6OwngoOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/stream": "^0.38.1", + "isomorphic-ws": "^4.0.1", + "ws": "^7", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/stargate": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.38.1.tgz", + "integrity": "sha512-HxLMJxvjN8neJcN382ir1X+nrsmfYYzJ9RpibEOgzN50Ir3D1qW1q6pumjCZb4s834iB0FOlMA1F0LIklaRong==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/amino": "^0.38.1", + "@cosmjs/encoding": "^0.38.1", + "@cosmjs/math": "^0.38.1", + "@cosmjs/proto-signing": "^0.38.1", + "@cosmjs/stream": "^0.38.1", + "@cosmjs/tendermint-rpc": "^0.38.1", + "@cosmjs/utils": "^0.38.1", + "cosmjs-types": "^0.11.0" + } + }, + "node_modules/@cosmjs/stream": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.38.1.tgz", + "integrity": "sha512-YIruJ6XfPQooMZzy7fTMx1/JnAMgiGMg785Zxja+RntbxrsHyWYUdWGi9ji8uOHEgZNYErHlK2NnbF8VlCo1ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/tendermint-rpc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.38.1.tgz", + "integrity": "sha512-yoGfI1wcw986qqCJkD544HXB4YxVOvne5s1RZkO437RtgvpLgVVdTBvCB1JcgLDw25gpHMFe1dWe5Sn9LPzQxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cosmjs/crypto": "^0.38.1", + "@cosmjs/encoding": "^0.38.1", + "@cosmjs/json-rpc": "^0.38.1", + "@cosmjs/math": "^0.38.1", + "@cosmjs/socket": "^0.38.1", + "@cosmjs/stream": "^0.38.1", + "@cosmjs/utils": "^0.38.1", + "readonly-date-esm": "^2.0.0", + "xstream": "^11.14.0" + } + }, + "node_modules/@cosmjs/utils": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.38.1.tgz", + "integrity": "sha512-ccQ5in6IvsQ+o/SstUdQH1jCJ2+MkJPZK7A/EYwMAFcjV8vzOgJ97LVy6AT24nwdi0/iVa2nbAG+fitUEsgLcA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1676,6 +1787,13 @@ ], "license": "MIT" }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "dev": true, + "license": "MIT" + }, "node_modules/bigint-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", @@ -1710,6 +1828,39 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/bip174": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-3.0.0.tgz", + "integrity": "sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.9", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-7.0.1.tgz", + "integrity": "sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^3.0.0", + "bs58check": "^4.0.0", + "uint8array-tools": "^0.0.9", + "valibot": "^1.2.0", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/bn.js": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", @@ -1769,6 +1920,34 @@ "base-x": "^3.0.2" } }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, + "node_modules/bs58check/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bs58check/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1838,6 +2017,16 @@ "node": ">=18" } }, + "node_modules/chain-registry": { + "version": "2.0.169", + "resolved": "https://registry.npmjs.org/chain-registry/-/chain-registry-2.0.169.tgz", + "integrity": "sha512-fC0WWw1igynLN5jEakeFA5SfOaRJj6drQwyXgegrflU2y5jrwiZRdcDcvj0iSZhnu3sRAloYfYaNaS/btAhp+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@chain-registry/types": "^2.0.169" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1960,6 +2149,13 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/coinselect": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/coinselect/-/coinselect-3.1.13.tgz", + "integrity": "sha512-iJOrKH/7N9gX0jRkxgOHuGjvzvoxUMSeylDhH1sHn+CjLjdin5R0Hz2WEBu/jrZV5OrHcm+6DMzxwu9zb5mSZg==", + "dev": true, + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1990,6 +2186,16 @@ "node": ">=18" } }, + "node_modules/cosmjs-types": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.11.0.tgz", + "integrity": "sha512-kDSkgHpRTrg1413jCNehT3P21+EBxZWFMBr9JEzVfmPiNdtuwAoLAkCYo7c7i/pTakAwyHsXbxOg8kkD+AN33w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20.19" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2060,6 +2266,42 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delay": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", @@ -2109,6 +2351,26 @@ "dev": true, "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -2621,6 +2883,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2631,6 +2923,26 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3264,6 +3576,16 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3551,6 +3873,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/readonly-date-esm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/readonly-date-esm/-/readonly-date-esm-2.0.0.tgz", + "integrity": "sha512-adlyzz144ofU22kjnnRIN0HPPqIbc5IZvMmMuVtEgMY4mKgNyKqOVb4Fa2GUzE2By4TEPOYoGqDoKRyqmeEuPQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3762,8 +4091,66 @@ } }, "node_modules/signet.js": { - "resolved": "../signet.js", - "link": true + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/signet.js/-/signet.js-0.4.0.tgz", + "integrity": "sha512-lq1ea6tIme5nhj2z2uAUHDQ3NdpO+u66/JSffgEhYUcwIQGRyYWpaHTef5KZTS7lAStCZn8ck6qVu3vnl+ZNIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@coral-xyz/anchor": "^0.32.1", + "@cosmjs/amino": "^0.38.0", + "@cosmjs/crypto": "^0.38.0", + "@cosmjs/encoding": "^0.38.0", + "@cosmjs/proto-signing": "^0.38.0", + "@cosmjs/stargate": "^0.38.0", + "@noble/curves": "^2.0.1", + "@scure/base": "^2.0.0", + "@solana/web3.js": "^1.98.4", + "bitcoinjs-lib": "^7.0.0", + "chain-registry": "^2.0.0", + "coinselect": "^3.1.13", + "cosmjs-types": "^0.11.0", + "viem": "^2.47.4" + } + }, + "node_modules/signet.js/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/signet.js/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/signet.js/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/smol-toml": { "version": "1.6.0", @@ -3932,6 +4319,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/symbol-observable": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/text-encoding-utf-8": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", @@ -4127,6 +4524,16 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/unbash": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/unbash/-/unbash-2.2.0.tgz", @@ -4170,6 +4577,41 @@ "license": "MIT", "peer": true }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "dev": true, + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, + "node_modules/varuint-bitcoin/node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/viem": { "version": "2.47.4", "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.4.tgz", @@ -4417,6 +4859,17 @@ } } }, + "node_modules/xstream": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/xstream/-/xstream-11.14.0.tgz", + "integrity": "sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "globalthis": "^1.0.1", + "symbol-observable": "^2.0.3" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index aa65c5d..502ba5d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "knip": "^5.86.0", "mocha": "^11.7.5", "prettier": "^3.8.1", - "signet.js": "^0.3.1", + "signet.js": "^0.4.0", "ts-mocha": "^11.1.0", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", diff --git a/tests/helpers/mpc-signer.ts b/tests/helpers/mpc-signer.ts index a4c9cec..dbb4025 100644 --- a/tests/helpers/mpc-signer.ts +++ b/tests/helpers/mpc-signer.ts @@ -14,6 +14,7 @@ export interface MpcSigner { contract: MpcContract; evm: InstanceType; predecessor: string; + publicClient: ReturnType; } export function createMpcSigner(sepoliaPrivateKey: string, sepoliaRpcUrl: string): MpcSigner { @@ -34,7 +35,7 @@ export function createMpcSigner(sepoliaPrivateKey: string, sepoliaRpcUrl: string }); const evm = new EVM({ publicClient: publicClient as any, contract }); - return { contract, evm, predecessor: account.address }; + return { contract, evm, predecessor: account.address, publicClient }; } export async function deriveMpcEthAddress( @@ -57,17 +58,32 @@ export async function signMessageMpc( remainingAccountKeys: PublicKey[], indexedInstructions: IndexedInnerInstruction[], path: string -): Promise<{ signature: Buffer; recoveryId: number }> { +): Promise<{ signature: Buffer; recoveryId: number; sepoliaTxHash: string }> { const innerHash = computeInnerHash(programId, nonce, remainingAccountKeys, indexedInstructions); const { hashToSign } = await signer.evm.prepareMessageForSigning({ raw: innerHash }); - const rsv = await signer.contract.sign( - { payload: hashToSign, path, key_version: 1 }, - { sign: {}, retry: { delay: 5_000, retryCount: 12 } } - ); + const signArgs = { payload: hashToSign, path, key_version: 1 }; + const { txHash, requestId } = await signer.contract.createSignatureRequest(signArgs); + const receipt = await signer.publicClient.waitForTransactionReceipt({ hash: txHash }); + + const pollResult = await signer.contract.pollForRequestId({ + requestId, + payload: signArgs.payload, + path: signArgs.path, + keyVersion: signArgs.key_version, + fromBlock: receipt.blockNumber, + options: { delay: 5_000, retryCount: 12 }, + }); + + if (!pollResult || "error" in pollResult) { + throw new Error( + `MPC signature failed: ${pollResult ? JSON.stringify(pollResult) : "not found"}` + ); + } return { - signature: Buffer.concat([Buffer.from(rsv.r, "hex"), Buffer.from(rsv.s, "hex")]), - recoveryId: rsv.v - 27, + signature: Buffer.concat([Buffer.from(pollResult.r, "hex"), Buffer.from(pollResult.s, "hex")]), + recoveryId: pollResult.v - 27, + sepoliaTxHash: txHash, }; } diff --git a/tests/mpc-ecdsa-proxy.ts b/tests/mpc-ecdsa-proxy.ts index 494c78b..37df83e 100644 --- a/tests/mpc-ecdsa-proxy.ts +++ b/tests/mpc-ecdsa-proxy.ts @@ -47,10 +47,11 @@ describe("mpc-ecdsa-proxy", () => { const ethAddress = await deriveMpcEthAddress(signer, MPC_PATH, 1); const [walletPDA] = deriveWalletPDA(ethAddress, programId); - await program.methods + const initTxHash = await program.methods .initializeWallet(Array.from(ethAddress)) .accounts({ payer: payer.publicKey }) .rpc(); + console.log("Solana initializeWallet tx:", initTxHash); // Mint tokens to PDA const mint = await createMint(provider.connection, payer, payer.publicKey, null, 6); @@ -85,7 +86,7 @@ describe("mpc-ecdsa-proxy", () => { const indexed = toIndexedInnerInstructions([innerIx], remainingKeys); const nonce = 0n; - const { signature, recoveryId } = await signMessageMpc( + const { signature, recoveryId, sepoliaTxHash } = await signMessageMpc( signer, programId, nonce, @@ -93,8 +94,9 @@ describe("mpc-ecdsa-proxy", () => { indexed, MPC_PATH ); + console.log("Sepolia MPC sign tx:", sepoliaTxHash); - await program.methods + const executeTxHash = await program.methods .execute( Array.from(signature), recoveryId, @@ -104,6 +106,7 @@ describe("mpc-ecdsa-proxy", () => { .accounts({ walletState: walletPDA, payer: payer.publicKey }) .remainingAccounts(remaining) .rpc(); + console.log("Solana execute tx:", executeTxHash); // Verify tokens moved const recipientAccount = await getAccount(provider.connection, recipientAta.address);