diff --git a/.changeset/real-carrots-cry.md b/.changeset/real-carrots-cry.md new file mode 100644 index 000000000..f45f5ce27 --- /dev/null +++ b/.changeset/real-carrots-cry.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/sdk': minor +--- + +Wallet and transaction signing methods are now part of the sdk. diff --git a/apps/walletd/lib/testMocks.ts b/apps/walletd/lib/testMocks.ts index 679ef9e3e..ba3075e0c 100644 --- a/apps/walletd/lib/testMocks.ts +++ b/apps/walletd/lib/testMocks.ts @@ -308,7 +308,7 @@ export function getConsensusState() { return { index: { height: 45962, - ID: 'bid:000000000cb8ef1dfeb66afa78bc0b3b2d1a7a1df948efba22f7fc1a5571e79f', + id: 'bid:000000000cb8ef1dfeb66afa78bc0b3b2d1a7a1df948efba22f7fc1a5571e79f', }, prevTimestamps: [ '2023-11-28T11:34:49-05:00', diff --git a/libs/sdk/package.json b/libs/sdk/package.json index aa514dc65..a08e5e17b 100644 --- a/libs/sdk/package.json +++ b/libs/sdk/package.json @@ -3,7 +3,10 @@ "description": "SDK for interacting directly with the Sia network from browsers and web clients.", "version": "0.0.2", "license": "MIT", - "dependencies": {}, + "dependencies": { + "@siafoundation/react-walletd": "3.0.0", + "@siafoundation/types": "0.1.3" + }, "devDependencies": { "undici": "5.28.3" }, diff --git a/libs/sdk/src/sdk.ts b/libs/sdk/src/sdk.ts index 92c384e4c..948d8a4c6 100644 --- a/libs/sdk/src/sdk.ts +++ b/libs/sdk/src/sdk.ts @@ -6,6 +6,7 @@ export function getSDK() { const wasm = (global as any).sia as WASM return { rhp: wasm.rhp, + wallet: wasm.wallet, WebTransportClient, } } diff --git a/libs/sdk/src/types.ts b/libs/sdk/src/types.ts index ca34508f1..da69aa143 100644 --- a/libs/sdk/src/types.ts +++ b/libs/sdk/src/types.ts @@ -1,3 +1,6 @@ +import { ConsensusNetwork, ConsensusState } from '@siafoundation/react-walletd' +import { Transaction, UnlockConditions } from '@siafoundation/types' + type Currency = string type Signature = string type Address = string @@ -144,4 +147,57 @@ export type WASM = { error?: string } } + wallet: { + generateSeedPhrase: () => { + phrase?: string + error?: string + } + generateKeyPair: () => { + privateKey?: string + publicKey?: string + error?: string + } + keyPairFromSeedPhrase: ( + phrase: string, + index: number + ) => { + privateKey?: string + publicKey?: string + error?: string + } + standardUnlockConditions: (publicKey: string) => { + unlockConditions?: UnlockConditions + error?: string + } + standardUnlockHash: (publicKey: string) => { + address?: string + error?: string + } + addressFromUnlockConditions: (unlockConditions: UnlockConditions) => { + address?: string + error?: string + } + addressFromSpendPolicy: (spendPolicy: string) => { + address?: string + error?: string + } + encodeTransaction: (txn: Transaction) => { + encodedTransaction?: string + error?: string + } + signTransaction: ( + cs: ConsensusState, + cn: ConsensusNetwork, + txn: Transaction, + sigIndex: number, + privateKey: string + ) => { + transaction?: Transaction + error?: string + } + transactionId: (txn: Transaction) => { + id?: string + error?: string + } + } } diff --git a/libs/sdk/src/wallet.mock.ts b/libs/sdk/src/wallet.mock.ts new file mode 100644 index 000000000..f1e93e500 --- /dev/null +++ b/libs/sdk/src/wallet.mock.ts @@ -0,0 +1,379 @@ +import { Transaction } from '@siafoundation/types' + +export const seed = + '352ef42e07c0fe6e57d15ace7a7ac6cef8ddd187c76c1131fc172967e817ff58' + +export const privateKey = + '352ef42e07c0fe6e57d15ace7a7ac6cef8ddd187c76c1131fc172967e817ff58' + +export function getTransaction(): Transaction { + return { + siacoinInputs: [ + { + parentID: + 'scoid:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5', + unlockConditions: { + timelock: 0, + publicKeys: [ + 'ed25519:b5b9196a3c19f94982bcdba250a973181b22112437832a8f818f4aa73b8add74', + ], + signaturesRequired: 1, + }, + }, + ], + siacoinOutputs: [ + { + value: '95408980544305197274920800', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + ], + siafundInputs: [ + { + parentID: + 'sfoid:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852', + unlockConditions: { + timelock: 0, + publicKeys: [ + 'ed25519:8a7496aa59f17a4aae68c7e41e09d5ca94e64ba27f74cdb0b143f70dcc67b206', + ], + signaturesRequired: 1, + }, + claimAddress: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + ], + siafundOutputs: [ + { + value: 1, + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + }, + ], + minerFees: ['3930000000000000000000'], + } +} + +export function getToSign() { + return [ + 'h:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852', + 'h:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5', + ] +} + +export function getAddresses() { + return [ + { + id: 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + publicKey: + 'ed25519:b5b9196a3c19f94982bcdba250a973181b22112437832a8f818f4aa73b8add74', + index: 1, + walletId: 'ad18cbe1-3281-4ec7-a7ad-93615009fbbc', + }, + { + id: 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + publicKey: + 'ed25519:8a7496aa59f17a4aae68c7e41e09d5ca94e64ba27f74cdb0b143f70dcc67b206', + index: 2, + walletId: 'ad18cbe1-3281-4ec7-a7ad-93615009fbbc', + }, + { + id: 'addr:fc9bc3482711e9f83642d07be385c0d434892245842b4c3f3b83b26d42cec15fe1aaac1be1ff', + address: + 'addr:fc9bc3482711e9f83642d07be385c0d434892245842b4c3f3b83b26d42cec15fe1aaac1be1ff', + publicKey: + 'ed25519:e80ab90d5baab391ec2e8fe31bf100f7ca3d4b5e3055eacf86afd42ab05798ba', + index: 0, + walletId: 'ad18cbe1-3281-4ec7-a7ad-93615009fbbc', + }, + ] +} + +export function getSiacoinOutputs() { + return [ + { + id: 'h:31cf3ddc946d71d219fb1fbe9a11804e607b6d5ad1b4bf7b3678a2faa701a42e', + leafIndex: 157143, + merkleProof: [ + 'h:743645ee8b7bd0bc755f693472d8ad7fa3c5772f447fd9a46381f7851d22cb92', + 'h:9d613d671b45af9e850ba1cae69fa0fc86b218ee0d469eea7ec3df3e83c1ee2e', + 'h:9c114bd973790e6265836fd459882604494ff2656c79cdedb0173836e03fa88d', + 'h:677019b966d4e16a5f6304d49eed25adb2eb06c5bf589f4cac9e1195db348c58', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '992140000000000000000000', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 0, + }, + { + id: 'h:7ebd499ad589f2b5987b4fdb7fc8b6aa5fab6eaff3f604c61b66ec5777ad9366', + leafIndex: 157141, + merkleProof: [ + 'h:bdbb8d00932cfd2f08e9a32f322ae006583548784031447a6db03e593b619bf6', + 'h:42bb6b4c240897c17373914540cd11373a0b4d39f1c18f6bb939cd7055bb5106', + 'h:9c114bd973790e6265836fd459882604494ff2656c79cdedb0173836e03fa88d', + 'h:677019b966d4e16a5f6304d49eed25adb2eb06c5bf589f4cac9e1195db348c58', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '996070000000000000000000', + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + }, + maturityHeight: 0, + }, + { + id: 'h:a1c8769809d7122dd7d99bb7ef17a7e8919a8d6967fc1607364f57eb93d8aaf5', + leafIndex: 157144, + merkleProof: [ + 'h:e82ad5ed328410bfcb00ec4b8e1c28ac07a9da7c3038ef7ef8c0b262fd70323e', + 'h:8684f565bd83846bd2dd5fc177933635bbc901372a4a0cc72c975296453ff750', + 'h:951ba8356477c4621de211ee00e62496cff865f8daa75530867e4c9e4745cc30', + 'h:6cb63de6d3d47b25f96e88929cf87faec2afa1858c5afcffff3f59aa7533ab8b', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '55711757555233190591737', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 45923, + }, + { + id: 'h:b222428602c8382b67a769d17e1cdc0952f37f2441a872b92671a6ed76cf22f5', + leafIndex: 155364, + merkleProof: [ + 'h:e5d791d9e928201d72460af12ae664eabc8116926620d8014b5498637b59e993', + 'h:7159d4f5825b2357530e9c24bc9bd1b9bda961ea642460969bfa769d9ab84d9c', + 'h:08961a734e757612378b8afbd9d30631d3b48b5830e9631ddfed76b05215d64f', + 'h:68d3178e805c140913f987d0f56775f687a884d657d434f6f4443d536db4fdf6', + 'h:67b2e29b5b1071b8bdb431556ccd48fd3f5edad0a97a98037f79cf292ea8bd09', + 'h:b4f209894a2759c36fb1b62252cdbd1b7c90d61d86f587b5042269dc938734f1', + 'h:19b42d9757d8c3a80d64c630190f140c23887d64fd0dd482db6bf5b3986226d5', + 'h:559236122641cca96efbb897ef800c91513674af9341964b2aca16e5593aa134', + 'h:35d5d6c421b6d87dd3b15f4fe624215ba009952e4c348be727df8ebb14d54e8d', + 'h:b6af69cf68b309c8129c40f81ae2324ac6486a631d2de1bdbfcea6cac755df0e', + 'h:c1cf1a3ff946efc4f9cafa7f0ec25487c6cb2d9121f706c29f7e48b43790f8e7', + 'h:442f5a2b1a5fe821cc811e9e1b746ebefb5b50441fc510cfed1787105e957aa6', + 'h:02537976f2e05843dbab0a6c5d09447cd9cc76a391f31ce764bf3a85a513dab8', + ], + siacoinOutput: { + value: '95412910544305197274920800', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 45278, + }, + { + id: 'h:b77b2aa8466032d774b324734cd1998e58476ce173ad412960f3f952abdfcd6f', + leafIndex: 157142, + merkleProof: [ + 'h:031f4de5a38fbb2df09eb9321f5f230b517f67a1b9919a9ea2f82eb9ad210f95', + 'h:9d613d671b45af9e850ba1cae69fa0fc86b218ee0d469eea7ec3df3e83c1ee2e', + 'h:9c114bd973790e6265836fd459882604494ff2656c79cdedb0173836e03fa88d', + 'h:677019b966d4e16a5f6304d49eed25adb2eb06c5bf589f4cac9e1195db348c58', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siacoinOutput: { + value: '47992140000000000000000000', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 0, + }, + { + id: 'h:f961362f6e60e9b85e33b77204ffdb9aec1f601d7e5b709ca2a41f1a048fd899', + leafIndex: 155372, + merkleProof: [ + 'h:26d070a7270455443e5f171dc75319a304d58265ebb3581229ce6bca842e403a', + 'h:d38fd124fb45cf5bd8ecfbfed3634ee93bea6478bd3504d611dd73a69a129d8a', + 'h:98d22e8c71ffece777e343aaddb97fe96f552a3762e4290c0ce69b73462ce192', + 'h:eac2f8bcf3663ebe914cf9721bef97b3d5f9422620a3c1aefd7c429846b0c044', + 'h:67b2e29b5b1071b8bdb431556ccd48fd3f5edad0a97a98037f79cf292ea8bd09', + 'h:b4f209894a2759c36fb1b62252cdbd1b7c90d61d86f587b5042269dc938734f1', + 'h:19b42d9757d8c3a80d64c630190f140c23887d64fd0dd482db6bf5b3986226d5', + 'h:559236122641cca96efbb897ef800c91513674af9341964b2aca16e5593aa134', + 'h:35d5d6c421b6d87dd3b15f4fe624215ba009952e4c348be727df8ebb14d54e8d', + 'h:b6af69cf68b309c8129c40f81ae2324ac6486a631d2de1bdbfcea6cac755df0e', + 'h:c1cf1a3ff946efc4f9cafa7f0ec25487c6cb2d9121f706c29f7e48b43790f8e7', + 'h:442f5a2b1a5fe821cc811e9e1b746ebefb5b50441fc510cfed1787105e957aa6', + 'h:02537976f2e05843dbab0a6c5d09447cd9cc76a391f31ce764bf3a85a513dab8', + ], + siacoinOutput: { + value: '0', + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + maturityHeight: 45280, + }, + ] +} + +export function getSiafundOutputs() { + return [ + { + id: 'h:425a60eee280854b7f3eb59b1613370bcc0ae3a02859f866f80e7b310475e1e8', + leafIndex: 155367, + merkleProof: [ + 'h:baf7bd62d901ae9b36ee5bb81bc9c9df1a06b2b24a92c91fc5006fbcfa989add', + 'h:3cf19954058b6c174edec23237efa0bb81cf71efeb9af73cbb6a8514d3a8028e', + 'h:08961a734e757612378b8afbd9d30631d3b48b5830e9631ddfed76b05215d64f', + 'h:68d3178e805c140913f987d0f56775f687a884d657d434f6f4443d536db4fdf6', + 'h:67b2e29b5b1071b8bdb431556ccd48fd3f5edad0a97a98037f79cf292ea8bd09', + 'h:b4f209894a2759c36fb1b62252cdbd1b7c90d61d86f587b5042269dc938734f1', + 'h:19b42d9757d8c3a80d64c630190f140c23887d64fd0dd482db6bf5b3986226d5', + 'h:559236122641cca96efbb897ef800c91513674af9341964b2aca16e5593aa134', + 'h:35d5d6c421b6d87dd3b15f4fe624215ba009952e4c348be727df8ebb14d54e8d', + 'h:b6af69cf68b309c8129c40f81ae2324ac6486a631d2de1bdbfcea6cac755df0e', + 'h:c1cf1a3ff946efc4f9cafa7f0ec25487c6cb2d9121f706c29f7e48b43790f8e7', + 'h:442f5a2b1a5fe821cc811e9e1b746ebefb5b50441fc510cfed1787105e957aa6', + 'h:02537976f2e05843dbab0a6c5d09447cd9cc76a391f31ce764bf3a85a513dab8', + ], + siafundOutput: { + value: 99, + address: + 'addr:934b885229a9f98153401d7a647a1862aede399c656f33ec8492dfffca557ca907a3d22089c8', + }, + claimStart: '32152366120469300091412640000', + }, + { + id: 'h:b53e88ce69f19f0bf1d3496479f20b72e1133c719e82278830ee6618bb582852', + leafIndex: 157146, + merkleProof: [ + 'h:01a92b66744b2a0198ef7a325268d8cae43858ca6f1d2677f9059327cedf6640', + 'h:19fecb42a55c9ed127354db7eaddbdf784e0dce3f2cd5d931e30d31c15bd6311', + 'h:951ba8356477c4621de211ee00e62496cff865f8daa75530867e4c9e4745cc30', + 'h:6cb63de6d3d47b25f96e88929cf87faec2afa1858c5afcffff3f59aa7533ab8b', + 'h:e96f6ae41fcec0f989ca00fc8697326b497515b5204569a2c21e17d4aa2e41ed', + 'h:00b971528955045e0c3ce29675188c5d2b4ddae18b78103aa2a3ae04c9fa2298', + 'h:73a8e33614511b21d98b49df6d96e92413dad51ba48007bd56a39692244e936d', + 'h:7b42dc5e0f6cfd84104bbae8279f917ae4fc383b6da5b792106e27559e14a4d5', + 'h:913008cd339f08d4c2c28e734ab617ae2917b838c67f8afa4d2d38043ca51aa3', + ], + siafundOutput: { + value: 1, + address: + 'addr:eb2ee5169dd9aaab804b38f7e70043690ac21da1144990a4a28c1dcf66cd7ee9845aef03006f', + }, + claimStart: '32709483696021631997330010000', + }, + ] +} + +export function getConsensusState() { + return { + index: { + height: 45962, + id: 'bid:000000000cb8ef1dfeb66afa78bc0b3b2d1a7a1df948efba22f7fc1a5571e79f', + }, + prevTimestamps: [ + '2023-11-28T11:34:49-05:00', + '2023-11-28T11:22:41-05:00', + '2023-11-28T11:19:59-05:00', + '2023-11-28T11:10:13-05:00', + '2023-11-28T11:09:32-05:00', + '2023-11-28T11:07:38-05:00', + '2023-11-28T10:47:27-05:00', + '2023-11-28T09:58:20-05:00', + '2023-11-28T09:51:26-05:00', + '2023-11-28T09:50:31-05:00', + '2023-11-28T09:40:07-05:00', + ], + depth: + 'bid:00000000000203572d5b49ea0e554f31ba43d81854d4313433fbb59f6c0db0b3', + childTarget: + 'bid:00000001724087005d8de96a9feb9a37bd483392cbb691f9cc73b5c9d14cc861', + siafundPool: '33603845293260630383068710000', + oakTime: 117766000000000, + oakTarget: + 'bid:0000000001d8373aecb257ac55c0077f7fe0d8e7c02053cefe7215aa480fdc63', + foundationPrimaryAddress: + 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', + foundationFailsafeAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + totalWork: '139825201060364', + difficulty: '2969630008', + oakWork: '596072835270', + elements: { + numLeaves: 157715, + trees: [ + 'h:680fc2873f62d72a4b41f93f6d919ce6271265e04f8135549cb7d0bda5df08e2', + 'h:dbdd12bdea241c262c3a39a85d37c7cf44c858e031a17843638b60285d1777ba', + 'h:9b3f0603ce5237d86e5a3ea3fcdf7b235c17ef4ad0ca7a66623e0df30bd6be62', + 'h:cf7b5de9eecc85f208d137d56b5642d193d378478fa49476af6f2d232883f552', + 'h:670e74aafd7bffe4b07d9a5c6c52111d0aadbc4cf0d76c00a8f2b8ce345999c3', + 'h:b2a0a932a907641d183201aee929b128808a980a3f08db9eba19e50e978b9bdb', + 'h:ca27be5aae09ffc0e932ce785770723c85dc0598c3e578d0b61a045245323a6f', + ], + }, + attestations: 0, + } +} + +export function getConsensusNetwork() { + return { + name: 'zen', + initialCoinbase: '300000000000000000000000000000', + minimumCoinbase: '300000000000000000000000000000', + initialTarget: + 'bid:0000000100000000000000000000000000000000000000000000000000000000', + hardforkDevAddr: { + height: 1, + oldAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + newAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + }, + hardforkTax: { + height: 2, + }, + hardforkStorageProof: { + height: 5, + }, + hardforkOak: { + height: 10, + fixHeight: 12, + genesisTimestamp: '2023-01-13T03:53:20-05:00', + }, + hardforkASIC: { + height: 20, + oakTime: 10000000000000, + oakTarget: + 'bid:0000000100000000000000000000000000000000000000000000000000000000', + }, + hardforkFoundation: { + height: 30, + primaryAddress: + 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', + failsafeAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + }, + hardforkV2: { + allowHeight: 100000, + requireHeight: 102000, + }, + } as const +} diff --git a/libs/sdk/src/wallet.spec.ts b/libs/sdk/src/wallet.spec.ts new file mode 100644 index 000000000..4f713efec --- /dev/null +++ b/libs/sdk/src/wallet.spec.ts @@ -0,0 +1,165 @@ +import { initSDKTest } from './initTest' +import { + getConsensusNetwork, + getConsensusState, + getTransaction, + privateKey, +} from './wallet.mock' + +describe('wallet', () => { + it('generateSeedPhrase', async () => { + const sdk = await initSDKTest() + const { phrase, error } = sdk.wallet.generateSeedPhrase() + expect(error).toBeUndefined() + expect(phrase?.split(' ').length).toBe(12) + }) + it('generateKeyPair', async () => { + const sdk = await initSDKTest() + const { privateKey, publicKey, error } = sdk.wallet.generateKeyPair() + expect(error).toBeUndefined() + expect(privateKey).toBeDefined() + expect(publicKey).toBeDefined() + }) + it('keyPairFromSeedPhrase', async () => { + const sdk = await initSDKTest() + const index0 = sdk.wallet.keyPairFromSeedPhrase( + 'bundle castle coil dismiss patient patrol blind erode future pave eyebrow manual', + 0 + ) + expect(index0.error).toBeUndefined() + expect(index0.privateKey).toBe( + '61ca130e51261fc8657fe20616c79c69188c0d28048cb8b09010682e65bb346bec5f92330370362126df379ac6235dc3e4e2d03822922646051a341e8fcb4035' + ) + expect(index0.publicKey).toBe( + 'ed25519:ec5f92330370362126df379ac6235dc3e4e2d03822922646051a341e8fcb4035' + ) + const index1 = sdk.wallet.keyPairFromSeedPhrase( + 'bundle castle coil dismiss patient patrol blind erode future pave eyebrow manual', + 1 + ) + expect(index1.error).toBeUndefined() + expect(index1.privateKey).toBe( + '5c797a530f532c967a6097922b39d43407ec1f8cc8e04316a0458b63f1ba06e88aed9bed8227bcd8ed6b0671b13f3b5a7d7c4f0636353372ccd9b037759ce6c7' + ) + expect(index1.publicKey).toBe( + 'ed25519:8aed9bed8227bcd8ed6b0671b13f3b5a7d7c4f0636353372ccd9b037759ce6c7' + ) + }) + describe('standardUnlockConditions', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const { error, unlockConditions } = sdk.wallet.standardUnlockConditions( + 'ed25519:ec5f92330370362126df379ac6235dc3e4e2d03822922646051a341e8fcb4035' + ) + expect(error).toBeUndefined() + expect(unlockConditions).toEqual({ + publicKeys: [ + 'ed25519:ec5f92330370362126df379ac6235dc3e4e2d03822922646051a341e8fcb4035', + ], + signaturesRequired: 1, + timelock: 0, + }) + }) + it('invalid', async () => { + const sdk = await initSDKTest() + const { error, unlockConditions } = + sdk.wallet.standardUnlockConditions('invalid') + expect(error).toEqual('invalid public key') + expect(unlockConditions).toBeUndefined() + }) + }) + describe('standardUnlockHash', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const { error, address } = sdk.wallet.standardUnlockHash( + 'ed25519:ec5f92330370362126df379ac6235dc3e4e2d03822922646051a341e8fcb4035' + ) + expect(error).toBeUndefined() + expect(address).toBe( + 'addr:69e52c7d3f57df42b9736a1b5a0e67ab2e4a1cda4b0e5c0c858929a5284d843278a7ce009198' + ) + }) + it('invalid', async () => { + const sdk = await initSDKTest() + const { error, address } = sdk.wallet.standardUnlockHash('invalid') + expect(error).toEqual('invalid public key') + expect(address).toBeUndefined() + }) + }) + describe('addressFromUnlockConditions', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const { error, address } = sdk.wallet.addressFromUnlockConditions({ + publicKeys: [ + 'ed25519:ec5f92330370362126df379ac6235dc3e4e2d03822922646051a341e8fcb4035', + ], + signaturesRequired: 1, + timelock: 0, + }) + expect(error).toBeUndefined() + expect(address).toBe( + 'addr:69e52c7d3f57df42b9736a1b5a0e67ab2e4a1cda4b0e5c0c858929a5284d843278a7ce009198' + ) + }) + it('invalid', async () => { + const sdk = await initSDKTest() + const { error, address } = sdk.wallet.addressFromUnlockConditions({ + publicKeys: ['invalid public key'], + signaturesRequired: 1, + timelock: 0, + }) + expect(error).toEqual( + 'decoding : failed: wrong number of separators' + ) + expect(address).toBeUndefined() + }) + }) + describe('addressFromSpendPolicy', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const spendPolicy = + 'thresh(1, [above(100),pk(0x5e4bbc181bae781575a30fabdce472842d0373c12eafcd8013dba0cbf69e34e0)])' + const { error, address } = sdk.wallet.addressFromSpendPolicy(spendPolicy) + expect(error).toBeUndefined() + console.log(address) + expect(address).toEqual( + 'addr:170bf8f730c072a881edf9be3c08d0491f23810f7344125444ebff4bd8855d98dd9159fe1cea' + ) + }) + }) + describe('encodeTransaction', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const txn = getTransaction() + const { error, encodedTransaction } = sdk.wallet.encodeTransaction(txn) + expect(error).toBeUndefined() + expect(encodedTransaction).toBeDefined() + }) + }) + describe('transactionId', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const txn = getTransaction() + const { error, id } = sdk.wallet.transactionId(txn) + expect(error).toBeUndefined() + expect(id).toEqual( + 'txid:1752b8ae52c6ba5ffabba2b0cac2cb2a39c6d9e7cb416d240c2da76878a8701e' + ) + }) + }) + // TODO: add signTransaction tests + describe('signTransaction', () => { + it('signs a valid transaction', async () => { + const sdk = await initSDKTest() + expect( + sdk.wallet.signTransaction( + getConsensusState(), + getConsensusNetwork(), + getTransaction(), + 0, + privateKey + ) + ).toMatchSnapshot() + }) + }) +}) diff --git a/sdk/main.go b/sdk/main.go index ed9754d4c..341e7bf0f 100644 --- a/sdk/main.go +++ b/sdk/main.go @@ -4,6 +4,7 @@ import ( "syscall/js" "go.sia.tech/web/sdk/rhp" + "go.sia.tech/web/sdk/wallet" ) func main() { @@ -26,6 +27,18 @@ func main() { "encodeWriteSectorResponse": jsFunc(rhp.EncodeWriteSectorResponse), "decodeWriteSectorResponse": jsFunc(rhp.DecodeWriteSectorResponse), }, + "wallet": map[string]any{ + "generateSeedPhrase": jsFunc(wallet.GenerateSeedPhrase), + "generateKeyPair": jsFunc(wallet.GenerateKeyPair), + "keyPairFromSeedPhrase": jsFunc(wallet.KeyPairFromSeedPhrase), + "standardUnlockConditions": jsFunc(wallet.StandardUnlockConditions), + "standardUnlockHash": jsFunc(wallet.StandardUnlockHash), + "addressFromUnlockConditions": jsFunc(wallet.AddressFromUnlockConditions), + "addressFromSpendPolicy": jsFunc(wallet.AddressFromSpendPolicy), + "encodeTransaction": jsFunc(wallet.EncodeTransaction), + "transactionId": jsFunc(wallet.TransactionID), + "signTransaction": jsFunc(wallet.SignTransaction), + }, }) c := make(chan bool, 1) <-c diff --git a/sdk/wallet/wallet.go b/sdk/wallet/wallet.go new file mode 100644 index 000000000..6771426a3 --- /dev/null +++ b/sdk/wallet/wallet.go @@ -0,0 +1,209 @@ +package wallet + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "fmt" + "syscall/js" + + "go.sia.tech/core/consensus" + "go.sia.tech/core/types" + "go.sia.tech/coreutils/wallet" + "go.sia.tech/web/sdk/api" + "go.sia.tech/web/sdk/marshal" + "go.sia.tech/web/sdk/utils" +) + +func encodeToBytes(v types.EncoderTo) []byte { + var buf bytes.Buffer + e := types.NewEncoder(&buf) + v.EncodeTo(e) + e.Flush() + return buf.Bytes() +} + +// GenerateSeedPhrase returns a new seed phrase. +func GenerateSeedPhrase(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args); err != nil { + return api.ResultErr(err) + } + + return api.Result(map[string]any{ + "phrase": wallet.NewSeedPhrase(), + }) +} + +// GenerateKeyPair returns a new ed25519 key pair. +func GenerateKeyPair(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args); err != nil { + return api.ResultErr(err) + } + + pk := types.GeneratePrivateKey() + return api.Result(map[string]any{ + "privateKey": hex.EncodeToString(pk), + "publicKey": pk.PublicKey().String(), + }) +} + +// KeyPairFromSeedPhrase returns the key pair corresponding to the given seed phrase and index. +func KeyPairFromSeedPhrase(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeString, js.TypeNumber); err != nil { + return api.ResultErr(err) + } + + var seed [32]byte + if err := wallet.SeedFromPhrase(&seed, args[0].String()); err != nil { + return api.ResultErr(err) + } + priv := wallet.KeyFromSeed(&seed, uint64(args[1].Int())) + return api.Result(map[string]any{ + "privateKey": hex.EncodeToString(priv), + "publicKey": priv.PublicKey().String(), + }) +} + +// StandardUnlockConditions returns the unlock conditions for a standard v1 unlockhash. +func StandardUnlockConditions(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeString); err != nil { + return api.ResultErr(err) + } + + var pub types.PublicKey + if pub.UnmarshalText([]byte(args[0].String())) != nil { + return api.ResultErrStr("invalid public key") + } + uc := types.StandardUnlockConditions(pub) + obj, err := marshal.MarshalStruct(uc) + if err != nil { + return api.ResultErrStr(fmt.Sprintf("error marshaling unlock conditions: %s", err)) + } + return api.Result(map[string]any{ + "unlockConditions": obj, + }) +} + +// StandardUnlockHash returns the v1 unlockhash corresponding to the given public key. +func StandardUnlockHash(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeString); err != nil { + return api.ResultErr(err) + } + + var pub types.PublicKey + if pub.UnmarshalText([]byte(args[0].String())) != nil { + return api.ResultErrStr("invalid public key") + } + return api.Result(map[string]any{ + "address": types.StandardUnlockHash(pub).String(), + }) +} + +// AddressFromUnlockConditions returns the address corresponding to the given unlock conditions. +func AddressFromUnlockConditions(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return api.ResultErr(err) + } + + var uc types.UnlockConditions + if err := marshal.UnmarshalStruct(args[0], &uc); err != nil { + return api.ResultErr(err) + } + + return api.Result(map[string]any{ + "address": uc.UnlockHash().String(), + }) +} + +// AddressFromSpendPolicy returns the address of a spend policy. +func AddressFromSpendPolicy(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeString); err != nil { + return api.ResultErr(err) + } + + var sp types.SpendPolicy + if err := marshal.UnmarshalStruct(args[0], &sp); err != nil { + return api.ResultErr(err) + } + + return api.Result(map[string]any{ + "address": sp.Address().String(), + }) +} + +// EncodeTransaction returns the binary encoding of a transaction. +func EncodeTransaction(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return api.ResultErr(err) + } + + var txn types.Transaction + if err := marshal.UnmarshalStruct(args[0], &txn); err != nil { + return api.ResultErrStr(fmt.Sprintf("error decoding transaction: %s", err)) + } + + encoded := encodeToBytes(txn) + return api.Result(map[string]any{ + "encodedTransaction": marshal.MarshalUint8Array(encoded), + }) +} + +// SignTransaction returns the signature of a transaction. +func SignTransaction(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeObject, js.TypeObject, js.TypeObject, js.TypeNumber, js.TypeString); err != nil { + return api.ResultErr(err) + } + + var cs consensus.State + if err := marshal.UnmarshalStruct(args[0], &cs); err != nil { + return api.ResultErrStr(fmt.Sprintf("error decoding consensus state: %s", err)) + } + + var cn consensus.Network + if err := marshal.UnmarshalStruct(args[1], &cn); err != nil { + return api.ResultErrStr(fmt.Sprintf("error decoding consensus network: %s", err)) + } + + var txn types.Transaction + if err := marshal.UnmarshalStruct(args[2], &txn); err != nil { + return api.ResultErrStr(fmt.Sprintf("error decoding transaction: %s", err)) + } + cs.Network = &cn + + sigIndex := args[3].Int() + + buf, err := hex.DecodeString(args[4].String()) + if err != nil { + return api.ResultErrStr(fmt.Sprintf("error decoding private key: %s", err)) + } else if len(buf) != ed25519.PrivateKeySize { + return api.ResultErrStr(fmt.Sprintf("invalid private key length: %d", len(buf))) + } + privateKey := types.PrivateKey(buf) + + tsig := &txn.Signatures[sigIndex] + var sigHash types.Hash256 + if tsig.CoveredFields.WholeTransaction { + sigHash = cs.WholeSigHash(txn, tsig.ParentID, tsig.PublicKeyIndex, tsig.Timelock, tsig.CoveredFields.Signatures) + } else { + sigHash = cs.PartialSigHash(txn, tsig.CoveredFields) + } + return api.Result(map[string]any{ + "signature": privateKey.SignHash(sigHash).String(), + }) +} + +// TransactionID returns the ID of a transaction. +func TransactionID(this js.Value, args []js.Value) api.Result { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return api.ResultErr(err) + } + + var txn types.Transaction + if err := marshal.UnmarshalStruct(args[0], &txn); err != nil { + return api.ResultErrStr(fmt.Sprintf("error decoding transaction: %s", err)) + } + + return api.Result(map[string]any{ + "id": txn.ID().String(), + }) +}