diff --git a/interchain/README.md b/interchain/README.md index c6f31828..d17e7a6e 100644 --- a/interchain/README.md +++ b/interchain/README.md @@ -9,6 +9,8 @@ The `@avalanche-sdk/interchain` package provides a developer-friendly TypeScript - Type-safe ICM client for sending cross-chain messages - Works seamlessly with [`viem`](https://viem.sh/) wallet clients - Built-in support for Avalanche C-Chain and custom subnets +- Cross-Chain ERC20 tokens transfer (ICTT) +- Warp Message Building ## Requirements @@ -163,4 +165,90 @@ async function main() { } main().catch(console.error); -``` \ No newline at end of file +``` + +## Warp Message Parsing + +The SDK provides utilities for parsing and working with Warp messages, which are used for cross-chain communication in the Avalanche network. Warp messages are signed messages that can be verified across different chains. + +### Message Structure + +A Warp message consists of several components: +- Network ID +- Source Chain ID +- Addressed Call Payload + - Source Address + - Message Payload (specific to the message type) +- BitSet Signatures + +For more details on the format, refer to original [AvalancheGo docs](https://github.com/ava-labs/avalanchego/blob/ae36212/vms/platformvm/warp/README.md) + +### Supported Message Types + +The SDK currently supports parsing the following types of AddressedCall payload messages: + +1. **RegisterL1ValidatorMessage** +2. **L1ValidatorWeightMessage** +3. **L1ValidatorRegistrationMessage** +4. **SubnetToL1ConversionMessage** + +For more details on the format of message types, refer to original [ACP 77](https://github.com/avalanche-foundation/ACPs/blob/58c78c/ACPs/77-reinventing-subnets/README.md#p-chain-warp-message-payloads) + +### Working with Warp Messages + +You can parse Warp messages from their hex representation and access their components. The SDK provides type-safe methods to work with different message types. + +For detailed examples of working with Warp messages, see the [warp examples directory](https://github.com/ava-labs/avalanche-sdk-typescript/tree/main/interchain/examples/warp). + +### Example Usage + +Message types and related builder functions can imported from `/warp` sub-path as shown below. + +```typescript +import { WarpMessage, RegisterL1ValidatorMessage } from "@avalanche-sdk/interchain/warp"; + +const signedWarpMsgHex = '' + +// Parse a signed Warp message +const signedWarpMsg = WarpMessage.fromHex(signedWarpMsgHex); + +// Parse message from signed message, or AddressedCall payload, or the actual message +const registerL1ValidatorMsg = RegisterL1ValidatorMessage.fromHex(signedWarpMsgHex); + +// Convert back to hex +const hexBytes = registerL1ValidatorMsg.toHex(); +``` + +### Building Unsigned Messages + +You can also build the unsigned messages like `RegisterL1ValidatorMessage` or `L1ValidatorWeightMessage`. + +```typescript +import { + AddressedCall, + L1ValidatorWeightMessage, + WarpUnsignedMessage +} from "@avalanche-sdk/interchain/warp"; + +// building the L1ValidatorWeight message using values +const newL1ValidatorWeightMsg = L1ValidatorWeightMessage.fromValues( + '251q44yFiimeVSHaQbBk69TzoeYqKu9VagGtLVqo92LphUxjmR', + 4n, + 41n, +) + +// building AddressedCall payload from the above message +const addressedCallPayload = AddressedCall.fromValues( + '0x35F884853114D298D7aA8607f4e7e0DB52205f07', + newL1ValidatorWeightMsg.toHex() +) + +// building WarpUnsignedMessage from the above addressed call payload +const warpUnsignedMessage = WarpUnsignedMessage.fromValues( + 1, + '251q44yFiimeVSHaQbBk69TzoeYqKu9VagGtLVqo92LphUxjmR', + addressedCallPayload.toHex() +) +``` + +For more detailed examples and use cases, refer to the [warp examples](https://github.com/ava-labs/avalanche-sdk-typescript/tree/main/interchain/examples/warp) in the repository. diff --git a/interchain/examples/warp/buildingMessages/l1ValidatorWeight.ts b/interchain/examples/warp/buildingMessages/l1ValidatorWeight.ts new file mode 100644 index 00000000..79f36785 --- /dev/null +++ b/interchain/examples/warp/buildingMessages/l1ValidatorWeight.ts @@ -0,0 +1,45 @@ +import { AddressedCall, L1ValidatorWeightMessage, WarpUnsignedMessage } from "../../../src/warp"; + +/** + * Structure of the message: + * WarpSignedMessage + * - WarpUnsignedMessage + * - NetworkId + * - SourceChainId + * - AddressedCallPayload + * - SourceAddress + * - L1ValidatorWeightMessage + * - BitSet Signatures + */ + +// Ref: See tx hash (on Fuji - PChain) zphUhqvXtj8dkYxg2BqCdTPDUPWBJUQkb1XTp38u8zDZ9VjMW +const signedWarpMsgHex = '0000000000056b804f574b890cf9e0cb0f0f68591a394bba1696cf62b4e576e793d8509cc88600000058000000000001000000140feedc0de0000000000000000000000000000000000000360000000000038ccf9ef520784d2fa5d97fbf098b8b4e82ff19408ec423c2970a522ab04b3a0400000000000000040000000000000029000000000000000106a8206d76cf3fa7d65fec8464b0311dce9283d05bcf0ca7987cdf03a3a2f764691e01df4f6aaa3ff6b52e5b92fd3291e519f3fb50bad5d9697a39e34e2c3e99ea585f0332e9d13b4b6db7ecc58eee44c7f96e64371b1eebaa6f7c45bbf0937e68'; + +// parsing L1 validator weight message from the signed warp message hex +const parsedL1ValidatorWeightMsg = L1ValidatorWeightMessage.fromHex(signedWarpMsgHex) + +// re-building the same message using values +const newL1ValidatorWeightMsg = L1ValidatorWeightMessage.fromValues( + '251q44yFiimeVSHaQbBk69TzoeYqKu9VagGtLVqo92LphUxjmR', + 4n, + 41n, +) +console.log(newL1ValidatorWeightMsg); +console.log('This should be true:', parsedL1ValidatorWeightMsg.toHex() === newL1ValidatorWeightMsg.toHex()); + +// now, let's build an unsgined message from the above newL1ValidatorWeightMsg +const addressedCallPayload = AddressedCall.fromValues( + '0x35F884853114D298D7aA8607f4e7e0DB52205f07', + newL1ValidatorWeightMsg.toHex() +) +console.log(addressedCallPayload); + +// now, let's build a WarpUnsignedMessage from the above addressed call payload +const warpUnsignedMessage = WarpUnsignedMessage.fromValues( + 1, + '251q44yFiimeVSHaQbBk69TzoeYqKu9VagGtLVqo92LphUxjmR', + addressedCallPayload.toHex() +) +console.log(warpUnsignedMessage); + +// We can sign the above unsgined message by sending it's hex value using .toHex() method diff --git a/interchain/examples/warp/l1ValidatorWeight.ts b/interchain/examples/warp/l1ValidatorWeight.ts new file mode 100644 index 00000000..3c0a4426 --- /dev/null +++ b/interchain/examples/warp/l1ValidatorWeight.ts @@ -0,0 +1,30 @@ +import { L1ValidatorWeightMessage, WarpMessage } from "../../src/warp"; + +/** + * Structure of the message: + * WarpSignedMessage + * - WarpUnsignedMessage + * - NetworkId + * - SourceChainId + * - AddressedCallPayload + * - SourceAddress + * - L1ValidatorWeightMessage + * - BitSet Signatures + */ + +// Ref: See tx hash (on Fuji - PChain) zphUhqvXtj8dkYxg2BqCdTPDUPWBJUQkb1XTp38u8zDZ9VjMW +const signedWarpMsgHex = '0000000000056b804f574b890cf9e0cb0f0f68591a394bba1696cf62b4e576e793d8509cc88600000058000000000001000000140feedc0de0000000000000000000000000000000000000360000000000038ccf9ef520784d2fa5d97fbf098b8b4e82ff19408ec423c2970a522ab04b3a0400000000000000040000000000000029000000000000000106a8206d76cf3fa7d65fec8464b0311dce9283d05bcf0ca7987cdf03a3a2f764691e01df4f6aaa3ff6b52e5b92fd3291e519f3fb50bad5d9697a39e34e2c3e99ea585f0332e9d13b4b6db7ecc58eee44c7f96e64371b1eebaa6f7c45bbf0937e68'; + +const signedWarpMsg = WarpMessage.fromHex(signedWarpMsgHex); +console.log(signedWarpMsg); + +// Directly extract the L1ValidatorWeightMessage +// `fromHex` static methods accepts: +// - fully signed message +// - addressed call payload +// - L1 validator weight message +const l1ValidatorWeightMsg = L1ValidatorWeightMessage.fromHex(signedWarpMsgHex) +console.log(l1ValidatorWeightMsg); + +// Use `.toHex()` method to get back the hex bytes +console.log(l1ValidatorWeightMsg.toHex()); diff --git a/interchain/examples/warp/registerL1Validator.ts b/interchain/examples/warp/registerL1Validator.ts new file mode 100644 index 00000000..99ec7414 --- /dev/null +++ b/interchain/examples/warp/registerL1Validator.ts @@ -0,0 +1,30 @@ +import { L1ValidatorWeightMessage, RegisterL1ValidatorMessage, WarpMessage } from "../../src/warp"; + +/** + * Structure of the message: + * WarpSignedMessage + * - WarpUnsignedMessage + * - NetworkId + * - SourceChainId + * - AddressedCallPayload + * - SourceAddress + * - RegisterL1ValidatorMessage + * - BitSet Signatures + */ + +// Ref: See tx hash (on Fuji - PChain) qKx5qy1zriGsWGhnRfHibE9kuDSUMfQV7gPQVCQ8iBKT5ZriV +const signedWarpMsgHex = '0x0000000000057f78fe8ca06cefa186ef29c15231e45e1056cd8319ceca0695ca61099e610355000000d80000000000010000001433b9785e20ec582d5009965fb3346f1716e8a423000000b60000000000015e8b6e2e8155e93739f2fa6a7f8a32c6bb2e1dce2e471b56dcc60aac49bf34350000001447b37278e32917ffc6d2861b50dd9751b4016dd1b0d305fd70c376b0f5d4e6b9184728dcacb7390f477015690133a5632affab5701e9ebe61038d2e41373de53f4569fd60000000067d1ac310000000100000001380c1fb1db38f176b50e77eca240258e31a5b5e80000000100000001380c1fb1db38f176b50e77eca240258e31a5b5e80000000000004e200000000000000003c4411899be0450aee4dcc1be90a8802bdbd12821a5025a74cb094ff0033982e7f3951d6c4b882a6ce39bd2aa835b31accd09c60f26bc75308af4e05c4237df9b72b04c2697c5a0a7fb0f05f7b09358743a4a2df8cd4eda61f0dea0312a7014baa8a5c1'; + +const signedWarpMsg = WarpMessage.fromHex(signedWarpMsgHex); +console.log(signedWarpMsg); + +// Directly extract the RegisterL1ValidatorMessage +// `fromHex` static methods accepts: +// - fully signed message +// - addressed call payload +// - RegisterL1ValidatorMessage +const registerL1ValidatorMsg = RegisterL1ValidatorMessage.fromHex(signedWarpMsgHex) +console.log(registerL1ValidatorMsg); + +// Use `.toHex()` method to get back the hex bytes +console.log(registerL1ValidatorMsg.toHex()); diff --git a/interchain/package-lock.json b/interchain/package-lock.json index c08da45f..6b4c7a48 100644 --- a/interchain/package-lock.json +++ b/interchain/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1-alpha.1", "license": "BSD-3-Clause", "dependencies": { + "@avalabs/avalanchejs": "5.1.0-alpha.1", "@avalanche-sdk/client": "^0.0.4-alpha.12" }, "devDependencies": { @@ -25,12 +26,13 @@ "node_modules/@adraffy/ens-normalize": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==" + "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", + "license": "MIT" }, "node_modules/@avalabs/avalanchejs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@avalabs/avalanchejs/-/avalanchejs-5.0.0.tgz", - "integrity": "sha512-0hJK/Hdf8v+q05c8+5K6arFmzq7o1W4I05/Dmr+Es1XRi8canvTu1Y0RruYd6ea2rrvX3UhKrPs3BzLhCTHDrw==", + "version": "5.1.0-alpha.1", + "resolved": "https://registry.npmjs.org/@avalabs/avalanchejs/-/avalanchejs-5.1.0-alpha.1.tgz", + "integrity": "sha512-nX8RDRrDvZv5SRGElD6FCYjHHeBR43iBHRoVtOmj5FQtHXNLjV2t8lLRD7/qoeV2LFIfG1b0pRk7bmEn305oZA==", "dependencies": { "@noble/curves": "1.3.0", "@noble/hashes": "1.3.3", @@ -42,39 +44,6 @@ "node": ">=20" } }, - "node_modules/@avalabs/avalanchejs/node_modules/@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.3" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@avalabs/avalanchejs/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@avalabs/avalanchejs/node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@avalanche-sdk/client": { "version": "0.0.4-alpha.12", "resolved": "https://registry.npmjs.org/@avalanche-sdk/client/-/client-0.0.4-alpha.12.tgz", @@ -91,16 +60,19 @@ "npm": ">=10" } }, - "node_modules/@avalanche-sdk/client/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "license": "MIT", - "engines": { - "node": ">= 16" + "node_modules/@avalanche-sdk/client/node_modules/@avalabs/avalanchejs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@avalabs/avalanchejs/-/avalanchejs-5.0.0.tgz", + "integrity": "sha512-0hJK/Hdf8v+q05c8+5K6arFmzq7o1W4I05/Dmr+Es1XRi8canvTu1Y0RruYd6ea2rrvX3UhKrPs3BzLhCTHDrw==", + "dependencies": { + "@noble/curves": "1.3.0", + "@noble/hashes": "1.3.3", + "@noble/secp256k1": "2.0.0", + "@scure/base": "1.1.5", + "micro-eth-signer": "0.7.2" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=20" } }, "node_modules/@ethereumjs/rlp": { @@ -119,6 +91,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, @@ -127,25 +100,24 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" + "@noble/hashes": "1.3.3" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -164,9 +136,10 @@ "license": "MIT" }, "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } @@ -175,6 +148,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", @@ -184,10 +158,47 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz", + "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" @@ -196,10 +207,31 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@types/node": { - "version": "20.19.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", - "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "version": "20.19.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", + "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -210,6 +242,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/wevm" }, @@ -352,7 +385,8 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" }, "node_modules/for-each": { "version": "0.3.5", @@ -573,6 +607,7 @@ "url": "https://github.com/sponsors/wevm" } ], + "license": "MIT", "peerDependencies": { "ws": "*" } @@ -599,39 +634,6 @@ "micro-packed": "~0.5.1" } }, - "node_modules/micro-eth-signer/node_modules/@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.3" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/micro-eth-signer/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/micro-eth-signer/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/micro-packed": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.5.3.tgz", @@ -644,15 +646,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/micro-packed/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ox": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/ox/-/ox-0.8.6.tgz", @@ -683,6 +676,33 @@ } } }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz", + "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -727,9 +747,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -790,6 +810,33 @@ } } }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", + "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", @@ -815,6 +862,7 @@ "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/interchain/package.json b/interchain/package.json index 95847f01..ad99a4d8 100644 --- a/interchain/package.json +++ b/interchain/package.json @@ -35,6 +35,7 @@ }, "homepage": "https://github.com/ava-labs/avalanche-sdk-typescript#readme", "dependencies": { + "@avalabs/avalanchejs": "5.1.0-alpha.1", "@avalanche-sdk/client": "^0.0.4-alpha.12" }, "peerDependencies": { @@ -49,7 +50,8 @@ ".": "./dist/index.js", "./chains": "./dist/chains/index.js", "./icm": "./dist/icm/index.js", - "./ictt": "./dist/ictt/index.js" + "./ictt": "./dist/ictt/index.js", + "./warp": "./dist/warp/index.js" }, "typesVersions": { "*": { @@ -61,6 +63,9 @@ ], "ictt": [ "dist/ictt/index.d.ts" + ], + "warp": [ + "dist/warp/index.d.ts" ] } } diff --git a/interchain/src/chains/index.ts b/interchain/src/chains/index.ts index f3e86e27..e04fee72 100644 --- a/interchain/src/chains/index.ts +++ b/interchain/src/chains/index.ts @@ -1,2 +1,3 @@ +export type { ChainConfig } from "./types/chainConfig"; export { avalancheFuji } from "./avalancheFuji"; export { dispatch } from "./dispatch"; diff --git a/interchain/src/index.ts b/interchain/src/index.ts index 7e54bead..4bb5263c 100644 --- a/interchain/src/index.ts +++ b/interchain/src/index.ts @@ -1,3 +1,4 @@ export * from './chains' export * from './icm' export * from './ictt' +export * from './warp' diff --git a/interchain/src/warp/addressedCallMessages/conversionData.ts b/interchain/src/warp/addressedCallMessages/conversionData.ts new file mode 100644 index 00000000..62e24e68 --- /dev/null +++ b/interchain/src/warp/addressedCallMessages/conversionData.ts @@ -0,0 +1,99 @@ +import { Address, BigIntPr, BlsPublicKey, Id, NodeId, pvmSerial, Short, utils } from "@avalabs/avalanchejs"; +import type { ValidatorData as ValidatorDataRaw } from "../types"; +import { sha256 } from "@noble/hashes/sha2"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a ConversionData (SubnetToL1Conversion) from a hex string. + * + * @param conversionDataHex - The hex string representing the ConversionData. + * @returns The parsed ConversionData instance. {@link ConversionData} + */ +export function parseConversionData( + conversionDataHex: string, +): ConversionData { + const parsedConversionData = warpManager.unpack( + utils.hexToBuffer(conversionDataHex), + pvmSerial.warp.AddressedCallPayloads.ConversionData, + ); + return new ConversionData( + parsedConversionData.subnetId, + parsedConversionData.managerChainId, + parsedConversionData.managerAddress, + parsedConversionData.validators, + ); +} + +/** + * Creates a new ConversionData from values. + * + * @param subnetId - The subnet ID (base58check encoded). + * @param managerChainId - The manager chain ID (base58check encoded). + * @param managerAddress - The manager address in Bech32 format. + * @param validators - An array of validator data. + * @param validators.nodeId - The node ID (base58check encoded). + * @param validators.blsPublicKey - The BLS public key in hex format. + * @param validators.weight - The weight of the validator as a bigint. + * @returns A new ConversionData instance. {@link ConversionData} + */ +export function newConversionData( + subnetId: string, + managerChainId: string, + managerAddress: string, + validators: ValidatorDataRaw[], +) { + const subnetIdBytes = utils.base58check.decode(subnetId); + const managerChainIdBytes = utils.base58check.decode(managerChainId); + const formattedValidators = validators.map((vldr) => new pvmSerial.warp.AddressedCallPayloads.ValidatorData( + new NodeId(utils.base58check.decode(vldr.nodeId)), + BlsPublicKey.fromHex(vldr.blsPublicKey), + new BigIntPr(vldr.weight), + )); + return new ConversionData( + new Id(subnetIdBytes), + new Id(managerChainIdBytes), + Address.fromHex(managerAddress), + formattedValidators, + ); +} + +/** + * ConversionData class provides utility methods to build + * and parse ConversionData from hex strings or values, and + * access its properties. + */ +export class ConversionData extends pvmSerial.warp.AddressedCallPayloads.ConversionData { + static fromHex(conversionDataHex: string) { + return parseConversionData(conversionDataHex); + } + + static fromValues( + subnetId: string, + managerChainId: string, + managerAddress: string, + validators: ValidatorDataRaw[], + ) { + return newConversionData(subnetId, managerChainId, managerAddress, validators); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + getConversionId() { + return utils.bufferToHex(sha256(utils.hexToBuffer(this.toHex()))); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [ConversionData, Uint8Array] { + throw new Error('Do not use `ConversionData.fromBytes` method directly.'); + } +} diff --git a/interchain/src/warp/addressedCallMessages/index.ts b/interchain/src/warp/addressedCallMessages/index.ts new file mode 100644 index 00000000..53dfcf0a --- /dev/null +++ b/interchain/src/warp/addressedCallMessages/index.ts @@ -0,0 +1,5 @@ +export { ConversionData, parseConversionData, newConversionData } from "./conversionData"; +export { L1ValidatorRegistrationMessage, parseL1ValidatorRegistrationMessage, newL1ValidatorRegistrationMessage } from "./l1ValidatorRegistrationMessage"; +export { L1ValidatorWeightMessage, parseL1ValidatorWeightMessage, newL1ValidatorWeightMessage } from "./l1ValidatorWeightMessage"; +export { RegisterL1ValidatorMessage, parseRegisterL1ValidatorMessage, newRegisterL1ValidatorMessage } from "./registerL1ValidatorMessage"; +export { SubnetToL1ConversionMessage, parseSubnetToL1ConversionMessage, newSubnetToL1ConversionMessage } from "./subnetToL1ConversionMessage"; diff --git a/interchain/src/warp/addressedCallMessages/l1ValidatorRegistrationMessage.ts b/interchain/src/warp/addressedCallMessages/l1ValidatorRegistrationMessage.ts new file mode 100644 index 00000000..f3ce6045 --- /dev/null +++ b/interchain/src/warp/addressedCallMessages/l1ValidatorRegistrationMessage.ts @@ -0,0 +1,83 @@ +import { Id, Bool, pvmSerial, utils, Short } from "@avalabs/avalanchejs"; +import { parseAddressedCallPayload } from "../addressedCallPayload"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a L1ValidatorRegistrationMessage (AddressedCall payload) from a hex string. + * + * @param l1ValidatorRegistrationMessageHex - The hex string representing the L1ValidatorRegistrationMessage. + * @returns The parsed L1ValidatorRegistrationMessage instance. {@link L1ValidatorRegistrationMessage} + */ +export function parseL1ValidatorRegistrationMessage( + l1ValidatorRegistrationMessageHex: string, +): L1ValidatorRegistrationMessage { + try { + const parsedL1ValidatorRegistrationMessage = warpManager.unpack( + utils.hexToBuffer(l1ValidatorRegistrationMessageHex), + pvmSerial.warp.AddressedCallPayloads.L1ValidatorRegistrationMessage, + ); + return new L1ValidatorRegistrationMessage( + parsedL1ValidatorRegistrationMessage.validationId, + parsedL1ValidatorRegistrationMessage.registered, + ); + } catch (error) { + const addressedCallPayload = parseAddressedCallPayload(l1ValidatorRegistrationMessageHex); + const l1ValidatorRegistrationMessage = parseL1ValidatorRegistrationMessage( + addressedCallPayload.payload.toString('hex'), + ); + return l1ValidatorRegistrationMessage; + } +} + +/** + * Creates a new L1ValidatorRegistrationMessage from values. + * + * @param validationId - The validation ID (base58check encoded). + * @param registered - The registration status as a boolean. + * @returns A new L1ValidatorRegistrationMessage instance. {@link L1ValidatorRegistrationMessage} + */ +export function newL1ValidatorRegistrationMessage( + validationId: string, + registered: boolean, +) { + const validationIdBytes = utils.base58check.decode(validationId); + return new L1ValidatorRegistrationMessage( + new Id(validationIdBytes), + new Bool(registered), + ); +} + +/** + * L1ValidatorRegistrationMessage class provides utility methods to build + * and parse L1ValidatorRegistrationMessage from hex strings or values, and + * access its properties. + */ +export class L1ValidatorRegistrationMessage extends pvmSerial.warp.AddressedCallPayloads.L1ValidatorRegistrationMessage { + static fromHex(l1ValidatorRegistrationMessageHex: string) { + return parseL1ValidatorRegistrationMessage(l1ValidatorRegistrationMessageHex); + } + + static fromValues( + validationId: string, + registered: boolean, + ) { + return newL1ValidatorRegistrationMessage(validationId, registered); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [L1ValidatorRegistrationMessage, Uint8Array] { + throw new Error('Do not use `L1ValidatorRegistrationMessage.fromBytes` method directly.'); + } +} diff --git a/interchain/src/warp/addressedCallMessages/l1ValidatorWeightMessage.ts b/interchain/src/warp/addressedCallMessages/l1ValidatorWeightMessage.ts new file mode 100644 index 00000000..6e9355d7 --- /dev/null +++ b/interchain/src/warp/addressedCallMessages/l1ValidatorWeightMessage.ts @@ -0,0 +1,80 @@ +import { BigIntPr, Id, pvmSerial, Short, utils } from "@avalabs/avalanchejs"; +import { parseAddressedCallPayload } from "../addressedCallPayload"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a L1ValidatorWeightMessage (AddressedCall payload) from a hex string. + * + * @param l1ValidatorWeightMessageHex - The hex string representing the L1ValidatorWeightMessage. + * @returns The parsed L1ValidatorWeightMessage instance. {@link L1ValidatorWeightMessage} + */ +export function parseL1ValidatorWeightMessage( + l1ValidatorWeightMessageHex: string, +): L1ValidatorWeightMessage { + try { + const parsedL1ValidatorWeightMessage = warpManager.unpack( + utils.hexToBuffer(l1ValidatorWeightMessageHex), + pvmSerial.warp.AddressedCallPayloads.L1ValidatorWeightMessage, + ); + return new L1ValidatorWeightMessage( + parsedL1ValidatorWeightMessage.validationId, + parsedL1ValidatorWeightMessage.nonce, + parsedL1ValidatorWeightMessage.weight + ); + } catch (error) { + const addressedCallPayload = parseAddressedCallPayload(l1ValidatorWeightMessageHex); + const l1ValidatorWeightMessage = parseL1ValidatorWeightMessage( + addressedCallPayload.payload.toString('hex'), + ); + return l1ValidatorWeightMessage; + } +} + +/** + * Creates a new L1ValidatorWeightMessage from values. + * + * @param validationId - The validation ID (base58check encoded). + * @param nonce - The nonce as a bigint. + * @param weight - The weight of the validator as a bigint. + * @returns A new L1ValidatorWeightMessage instance. {@link L1ValidatorWeightMessage} + */ +export function newL1ValidatorWeightMessage(validationId: string, nonce: bigint, weight: bigint) { + const validationIdBytes = utils.base58check.decode(validationId); + return new L1ValidatorWeightMessage( + new Id(validationIdBytes), + new BigIntPr(nonce), + new BigIntPr(weight) + ); +} + +/** + * L1ValidatorWeightMessage class provides utility methods to build + * and parse L1ValidatorWeightMessage from hex strings or values, and + * access its properties. + */ +export class L1ValidatorWeightMessage extends pvmSerial.warp.AddressedCallPayloads.L1ValidatorWeightMessage { + static fromHex(l1ValidatorWeightMessageHex: string) { + return parseL1ValidatorWeightMessage(l1ValidatorWeightMessageHex); + } + + static fromValues(validationId: string, nonce: bigint, weight: bigint) { + return newL1ValidatorWeightMessage(validationId, nonce, weight); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [L1ValidatorWeightMessage, Uint8Array] { + throw new Error('Do not use `L1ValidatorWeightMessage.fromBytes` method directly.'); + } +} diff --git a/interchain/src/warp/addressedCallMessages/registerL1ValidatorMessage.ts b/interchain/src/warp/addressedCallMessages/registerL1ValidatorMessage.ts new file mode 100644 index 00000000..91ba9efe --- /dev/null +++ b/interchain/src/warp/addressedCallMessages/registerL1ValidatorMessage.ts @@ -0,0 +1,128 @@ +import { + BigIntPr, + BlsPublicKey, + PChainOwner, + Id, + NodeId, + pvmSerial, + utils, + Short, +} from "@avalabs/avalanchejs"; +import { parseAddressedCallPayload } from "../addressedCallPayload"; +import { parseBech32AddressToBytes } from "../utils"; +import type { PChainOwner as PChainOwnerRaw } from "../types"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a RegisterL1ValidatorMessage (AddressedCall payload) from a hex string. + * + * @param registerL1ValidatorMessageHex - The hex string representing the RegisterL1ValidatorMessage. + * @returns The parsed RegisterL1ValidatorMessage instance. {@link RegisterL1ValidatorMessage} + */ +export function parseRegisterL1ValidatorMessage( + registerL1ValidatorMessageHex: string, +): RegisterL1ValidatorMessage { + try { + const parsedRegisterL1ValidatorMessage = warpManager.unpack( + utils.hexToBuffer(registerL1ValidatorMessageHex), + pvmSerial.warp.AddressedCallPayloads.RegisterL1ValidatorMessage, + ); + return new RegisterL1ValidatorMessage( + parsedRegisterL1ValidatorMessage.subnetId, + parsedRegisterL1ValidatorMessage.nodeId, + parsedRegisterL1ValidatorMessage.blsPublicKey, + parsedRegisterL1ValidatorMessage.expiry, + parsedRegisterL1ValidatorMessage.remainingBalanceOwner, + parsedRegisterL1ValidatorMessage.disableOwner, + parsedRegisterL1ValidatorMessage.weight + ); + } catch (error) { + const addressedCallPayload = parseAddressedCallPayload(registerL1ValidatorMessageHex); + const registerL1ValidatorMessage = parseRegisterL1ValidatorMessage( + addressedCallPayload.payload.toString('hex'), + ); + return registerL1ValidatorMessage; + } +} + +/** + * Creates a new RegisterL1ValidatorMessage from values. + * + * @param subnetId - The subnet ID (base58check encoded). + * @param nodeId - The node ID (base58check encoded). + * @param blsPublicKey - The BLS public key in hex format. + * @param expiry - The expiry time as a bigint. + * @param remainingBalanceOwner - The remaining balance owner details. + * @param disableOwner - The disable owner details. + * @param weight - The weight of the validator as a bigint. + * @returns A new RegisterL1ValidatorMessage instance. {@link RegisterL1ValidatorMessage} + */ +export function newRegisterL1ValidatorMessage( + subnetId: string, + nodeId: string, + blsPublicKey: string, + expiry: bigint, + remainingBalanceOwner: PChainOwnerRaw, + disableOwner: PChainOwnerRaw, + weight: bigint +) { + const nodeIdBytes = utils.base58check.decode(nodeId.replace('NodeID-', '')); + const subnetIdBytes = utils.base58check.decode(subnetId); + const formattedRemainingBalanceOwner = PChainOwner.fromNative( + remainingBalanceOwner.addresses.map((address) => parseBech32AddressToBytes(address, 'P')), + remainingBalanceOwner.threshold + ); + const formattedDisableOwner = PChainOwner.fromNative( + disableOwner.addresses.map((address) => parseBech32AddressToBytes(address, 'P')), + disableOwner.threshold + ); + return new RegisterL1ValidatorMessage( + new Id(subnetIdBytes), + new NodeId(nodeIdBytes), + BlsPublicKey.fromHex(blsPublicKey), + new BigIntPr(expiry), + formattedRemainingBalanceOwner, + formattedDisableOwner, + new BigIntPr(weight) + ) +} + +/** + * RegisterL1ValidatorMessage class provides utility methods to build + * and parse RegisterL1ValidatorMessage from hex strings or values, and + * access its properties. + */ +export class RegisterL1ValidatorMessage extends pvmSerial.warp.AddressedCallPayloads.RegisterL1ValidatorMessage { + static fromHex(registerL1ValidatorMessageHex: string) { + return parseRegisterL1ValidatorMessage(registerL1ValidatorMessageHex); + } + + static fromValues( + subnetId: string, + nodeId: string, + blsPublicKey: string, + expiry: bigint, + remainingBalanceOwner: PChainOwnerRaw, + disableOwner: PChainOwnerRaw, + weight: bigint + ) { + return newRegisterL1ValidatorMessage(subnetId, nodeId, blsPublicKey, expiry, remainingBalanceOwner, disableOwner, weight); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [RegisterL1ValidatorMessage, Uint8Array] { + throw new Error('Do not use `RegisterL1ValidatorMessage.fromBytes` method directly.'); + } +} diff --git a/interchain/src/warp/addressedCallMessages/subnetToL1ConversionMessage.ts b/interchain/src/warp/addressedCallMessages/subnetToL1ConversionMessage.ts new file mode 100644 index 00000000..35bb09dc --- /dev/null +++ b/interchain/src/warp/addressedCallMessages/subnetToL1ConversionMessage.ts @@ -0,0 +1,78 @@ +import { Id, pvmSerial, Short, utils } from "@avalabs/avalanchejs"; +import { parseAddressedCallPayload } from "../addressedCallPayload"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a SubnetToL1ConversionMessage (AddressedCall payload) from a hex string. + * + * @param subnetToL1ConversionMessageHex - The hex string representing the SubnetToL1ConversionMessage. + * @returns The parsed SubnetToL1ConversionMessage instance. {@link SubnetToL1ConversionMessage} + */ +export function parseSubnetToL1ConversionMessage( + subnetToL1ConversionMessageHex: string, +): SubnetToL1ConversionMessage { + try { + const parsedSubnetToL1ConversionMessage = warpManager.unpack( + utils.hexToBuffer(subnetToL1ConversionMessageHex), + pvmSerial.warp.AddressedCallPayloads.SubnetToL1ConversionMessage, + ); + return new SubnetToL1ConversionMessage( + parsedSubnetToL1ConversionMessage.conversionId, + ); + } catch (error) { + const addressedCallPayload = parseAddressedCallPayload(subnetToL1ConversionMessageHex); + const subnetToL1ConversionMessage = parseSubnetToL1ConversionMessage( + addressedCallPayload.payload.toString('hex'), + ); + return subnetToL1ConversionMessage; + } +} + +/** + * Creates a new SubnetToL1ConversionMessage from a conversion ID. + * + * @param conversionId - The conversion ID (base58check encoded). + * @returns A new SubnetToL1ConversionMessage instance. {@link SubnetToL1ConversionMessage} + */ +export function newSubnetToL1ConversionMessage( + conversionId: string, +) { + const conversionIdBytes = utils.base58check.decode(conversionId); + return new SubnetToL1ConversionMessage( + new Id(conversionIdBytes), + ) +} + +/** + * SubnetToL1ConversionMessage class provides utility methods to build + * and parse SubnetToL1ConversionMessage from hex strings or values, and + * access its properties. + */ +export class SubnetToL1ConversionMessage extends pvmSerial.warp.AddressedCallPayloads.SubnetToL1ConversionMessage { + static fromHex(subnetToL1ConversionMessageHex: string) { + return parseSubnetToL1ConversionMessage(subnetToL1ConversionMessageHex); + } + + static fromValues( + conversionId: string, + ) { + return newSubnetToL1ConversionMessage(conversionId); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [SubnetToL1ConversionMessage, Uint8Array] { + throw new Error('Do not use `SubnetToL1ConversionMessage.fromBytes` method directly.'); + } +} diff --git a/interchain/src/warp/addressedCallPayload.ts b/interchain/src/warp/addressedCallPayload.ts new file mode 100644 index 00000000..a6aa5036 --- /dev/null +++ b/interchain/src/warp/addressedCallPayload.ts @@ -0,0 +1,76 @@ +import { utils, pvmSerial, Address, Bytes, Short } from "@avalabs/avalanchejs"; +import { parseWarpMessage } from "./warpMessage"; +import { evmOrBech32AddressToBytes } from "./utils"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a Warp AddressedCall payload from a hex string. + * + * @param addressedCallPayloadHex - The hex string representing the AddressedCall payload. + * @returns The parsed AddressedCall instance. {@link AddressedCall} + */ +export function parseAddressedCallPayload( + addressedCallPayloadHex: string, +): AddressedCall { + try { + const parsedAddressedCallPayload = warpManager.unpack( + utils.hexToBuffer(addressedCallPayloadHex), + pvmSerial.warp.AddressedCallPayloads.AddressedCall, + ); + return new AddressedCall( + parsedAddressedCallPayload.sourceAddress, + parsedAddressedCallPayload.payload + ); + } catch (error) { + const warpMsg = parseWarpMessage(addressedCallPayloadHex); + const addressedCallPayload = parseAddressedCallPayload( + warpMsg.unsignedMessage.payload.toString('hex'), + ); + return addressedCallPayload; + } +} + +/** + * Creates a new AddressedCall from values. + * + * @param sourceAddress - The source address (EVM or Bech32 format). + * @param payloadHex - The payload as a hex string. + * @returns A new AddressedCall instance. {@link AddressedCall} + */ +export function newAddressedCallPayload(sourceAddress: string, payloadHex: string) { + const sourceAddressBytes = evmOrBech32AddressToBytes(sourceAddress); + const payloadBytes = utils.hexToBuffer(payloadHex); + return new AddressedCall(new Address(sourceAddressBytes), new Bytes(payloadBytes)); +} + +/** + * AddressedCall class provides utility methods to build + * and parse AddressedCall payloads from hex strings or values, and + * access its properties. + */ +export class AddressedCall extends pvmSerial.warp.AddressedCallPayloads.AddressedCall { + static fromHex(addressedCallPayloadHex: string): AddressedCall { + return parseAddressedCallPayload(addressedCallPayloadHex); + } + + static fromValues(sourceAddress: string, payloadHex: string) { + return newAddressedCallPayload(sourceAddress, payloadHex); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [AddressedCall, Uint8Array] { + throw new Error('Do not use `AddressedCall.fromBytes` method directly.'); + } +} \ No newline at end of file diff --git a/interchain/src/warp/index.ts b/interchain/src/warp/index.ts new file mode 100644 index 00000000..6439782c --- /dev/null +++ b/interchain/src/warp/index.ts @@ -0,0 +1,10 @@ +export { AddressedCall, parseAddressedCallPayload, newAddressedCallPayload } from "./addressedCallPayload"; +export { WarpMessage, parseWarpMessage } from "./warpMessage"; +export { WarpUnsignedMessage, parseWarpUnsignedMessage, newWarpUnsignedMessage } from "./warpUnsignedMessage"; + +// AddressedCall exports +export { ConversionData, parseConversionData, newConversionData } from "./addressedCallMessages/conversionData"; +export { L1ValidatorRegistrationMessage, parseL1ValidatorRegistrationMessage, newL1ValidatorRegistrationMessage } from "./addressedCallMessages/l1ValidatorRegistrationMessage"; +export { L1ValidatorWeightMessage, parseL1ValidatorWeightMessage, newL1ValidatorWeightMessage } from "./addressedCallMessages/l1ValidatorWeightMessage"; +export { RegisterL1ValidatorMessage, parseRegisterL1ValidatorMessage, newRegisterL1ValidatorMessage } from "./addressedCallMessages/registerL1ValidatorMessage"; +export { SubnetToL1ConversionMessage, parseSubnetToL1ConversionMessage, newSubnetToL1ConversionMessage } from "./addressedCallMessages/subnetToL1ConversionMessage"; diff --git a/interchain/src/warp/types.ts b/interchain/src/warp/types.ts new file mode 100644 index 00000000..bb6a6e3b --- /dev/null +++ b/interchain/src/warp/types.ts @@ -0,0 +1,10 @@ +export type PChainOwner = { + threshold: number; + addresses: string[]; +} + +export type ValidatorData = { + nodeId: string, + blsPublicKey: string, + weight: bigint, +} diff --git a/interchain/src/warp/utils.ts b/interchain/src/warp/utils.ts new file mode 100644 index 00000000..910c63cb --- /dev/null +++ b/interchain/src/warp/utils.ts @@ -0,0 +1,48 @@ +import { utils } from "@avalabs/avalanchejs"; + +/** + * Parses a bech32 address to bytes. + * @param bech32Address The bech32 address string to parse. + * @param chainAlias The chain alias to add to the address if it's not already present. + * @returns The bytes of the address. + */ +export function parseBech32AddressToBytes(bech32Address: string, chainAlias?: string) { + if (bech32Address.includes('-')) { + return utils.bech32ToBytes(bech32Address); + } + // separator not present, adding chainAlias + '-' if provided + if (chainAlias) { + return utils.bech32ToBytes(`${chainAlias}-${bech32Address}`); + } + throw new Error(`Invalid address: ${bech32Address}. No chain alias provided.`); +} + +export function evmAddressToBytes(address: string) { + let evmAddress = address; + if (!evmAddress.startsWith('0x')) { + evmAddress = `0x${evmAddress}`; + } + // EVM addresses are 20 bytes (0x + 40 chars) + if (evmAddress.length === 42) { + return utils.hexToBuffer(evmAddress); + } + throw new Error(`Invalid EVM address: ${address}`); +} + +export function bech32AddressToBytes(address: string) { + // Check if it's a Bech32 address (contains a hyphen) + if (address.includes('-')) { + return utils.bech32ToBytes(address); + } + + // If it's a Bech32 address without chain alias, add P- prefix + return utils.bech32ToBytes(`P-${address}`); +} + +export function evmOrBech32AddressToBytes(address: string) { + try { + return evmAddressToBytes(address); + } catch (error) { + return bech32AddressToBytes(address); + } +} diff --git a/interchain/src/warp/warpMessage.ts b/interchain/src/warp/warpMessage.ts new file mode 100644 index 00000000..848537d7 --- /dev/null +++ b/interchain/src/warp/warpMessage.ts @@ -0,0 +1,48 @@ +import { pvmSerial, Short, utils } from "@avalabs/avalanchejs"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a Warp signed message from a hex string. + * + * @param warpMsgHex - The hex string representing the signed message. + * @returns The parsed WarpSignedMessage instance. {@link WarpMessage} +*/ +export function parseWarpMessage(warpMsgHex: string): WarpMessage { + const parsedWarpMsg = warpManager.unpack( + utils.hexToBuffer(warpMsgHex), + pvmSerial.warp.WarpMessage, + ); + + return new WarpMessage( + parsedWarpMsg.unsignedMessage, + parsedWarpMsg.signature + ); +} + +/** + * WarpSignedMessage class provides utility methods to build + * and parse signed warp message from hex strings or values, and + * access its properties. + */ +export class WarpMessage extends pvmSerial.warp.WarpMessage { + static fromHex(warpMsgHex: string) { + return parseWarpMessage(warpMsgHex); + } + + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [WarpMessage, Uint8Array] { + throw new Error('Do not use `WarpMessage.fromBytes` method directly.'); + } +} diff --git a/interchain/src/warp/warpUnsignedMessage.ts b/interchain/src/warp/warpUnsignedMessage.ts new file mode 100644 index 00000000..3344df28 --- /dev/null +++ b/interchain/src/warp/warpUnsignedMessage.ts @@ -0,0 +1,104 @@ +import { utils, pvmSerial, Int, Id, Bytes, Short } from "@avalabs/avalanchejs"; +import { parseWarpMessage } from "./warpMessage"; + +const warpManager = pvmSerial.warp.getWarpManager(); + +/** + * Parses a Warp unsigned or signed message from a hex string. + * + * @param unsignedMsgHex - The hex string representing the unsigned or signed message. + * @returns The parsed WarpUnsignedMessage instance. {@link WarpUnsignedMessage} + */ +export function parseWarpUnsignedMessage(unsignedMsgHex: string): WarpUnsignedMessage { + try { + const parsedWarpUnsignedMsg = warpManager.unpack( + utils.hexToBuffer(unsignedMsgHex), + pvmSerial.warp.WarpUnsignedMessage, + ); + + return new WarpUnsignedMessage( + parsedWarpUnsignedMsg.networkId, + parsedWarpUnsignedMsg.sourceChainId, + parsedWarpUnsignedMsg.payload + ); + } catch (error) { + const warpMsg = parseWarpMessage(unsignedMsgHex); + const unsignedMsg = parseWarpUnsignedMessage( + warpMsg.unsignedMessage.payload.toString('hex'), + ); + return unsignedMsg; + } +} + +/** + * Creates a new WarpUnsignedMessage from values. + * + * @param networkId - The Avalanche network ID. + * @param sourceChainId - The source blockchain ID. + * @param payloadHex - The warp message payload as a hex string. + * @returns A new WarpUnsignedMessage instance. {@link WarpUnsignedMessage} + */ +export function newWarpUnsignedMessage( + networkId: number, + sourceChainId: string, + payloadHex: string, +) { + return new WarpUnsignedMessage( + new Int(networkId), + new Id(utils.base58check.decode(sourceChainId)), + new Bytes(utils.hexToBuffer(payloadHex)), + ); +} + +/** + * WarpUnsignedMessage class provides utility methods to build + * and parse unsigned warp message from hex strings or values, and + * access its properties. + */ +export class WarpUnsignedMessage extends pvmSerial.warp.WarpUnsignedMessage { + /** + * Creates a WarpUnsignedMessage instance from a hex string. + * @param unsignedMsgHex - The hex string representing the unsigned message. + * @returns The parsed WarpUnsignedMessage instance. {@link WarpUnsignedMessage} + */ + static fromHex(unsignedMsgHex: string) { + return parseWarpUnsignedMessage(unsignedMsgHex); + } + + /** + * Creates a WarpUnsignedMessage instance from values. + * @param networkId - The Avalanche network ID. + * @param sourceChainId - The source chain ID (base58check encoded). + * @param payloadHex - The payload as a hex string. + * @returns A new WarpUnsignedMessage instance. {@link WarpUnsignedMessage} + */ + static fromValues( + networkId: number, + sourceChainId: string, + payloadHex: string, + ) { + return newWarpUnsignedMessage(networkId, sourceChainId, payloadHex); + } + + /** + * Serializes the WarpUnsignedMessage to a hex string. + * @returns The hex string representation of the message. + */ + toHex() { + const bytesWithoutCodec = this.toBytes(pvmSerial.warp.codec) + const codecBytes = new Short(0) + return utils.bufferToHex(Buffer.concat([codecBytes.toBytes(), bytesWithoutCodec])); + } + + /** + * Do not use this method directly. + * Throws an error if called. + * @throws Error + */ + static override fromBytes( + _bytes: never, + _codec: never + ): [WarpUnsignedMessage, Uint8Array] { + throw new Error('Do not use `WarpUnsignedMessage.fromBytes` method directly.'); + } +}