diff --git a/.changeset/big-waves-press.md b/.changeset/big-waves-press.md new file mode 100644 index 00000000..1c4e5e9f --- /dev/null +++ b/.changeset/big-waves-press.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/wallet-api-core": minor +--- + +feat: add stacks family diff --git a/packages/core/package.json b/packages/core/package.json index 4d8831f7..fe013b99 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,6 +20,7 @@ }, "devDependencies": { "@ledgerhq/jest-shared-config": "workspace:*", + "@stacks/transactions": "^6.13.0", "@types/jest": "^29.5.12", "@types/node": "^20.11.19", "@types/uuid": "^9.0.8", diff --git a/packages/core/src/families/common.ts b/packages/core/src/families/common.ts index b11745e4..96d89e0f 100644 --- a/packages/core/src/families/common.ts +++ b/packages/core/src/families/common.ts @@ -27,6 +27,7 @@ export const FAMILIES = [ "cardano", "solana", "vechain", + "stacks", ] as const; export const schemaFamilies = z.enum(FAMILIES); diff --git a/packages/core/src/families/index.ts b/packages/core/src/families/index.ts index b985329e..e6d1735e 100644 --- a/packages/core/src/families/index.ts +++ b/packages/core/src/families/index.ts @@ -17,6 +17,7 @@ export * from "./elrond/types"; export * from "./cardano/types"; export * from "./solana/types"; export * from "./vechain/types"; +export * from "./stacks/types"; export * from "./common"; export * from "./serializer"; diff --git a/packages/core/src/families/serializer.ts b/packages/core/src/families/serializer.ts index 663a612e..1dce7e69 100644 --- a/packages/core/src/families/serializer.ts +++ b/packages/core/src/families/serializer.ts @@ -68,6 +68,10 @@ import { deserializeVechainTransaction, serializeVechainTransaction, } from "./vechain/serializer"; +import { + deserializeStacksTransaction, + serializeStacksTransaction, +} from "./stacks/serializer"; import type { RawTransaction, Transaction } from "./types"; /** @@ -117,6 +121,8 @@ export function serializeTransaction(transaction: Transaction): RawTransaction { return serializeSolanaTransaction(transaction); case "vechain": return serializeVechainTransaction(transaction); + case "stacks": + return serializeStacksTransaction(transaction); default: { const exhaustiveCheck: never = transaction; // https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking return exhaustiveCheck; @@ -174,6 +180,8 @@ export function deserializeTransaction( return deserializeSolanaTransaction(rawTransaction); case "vechain": return deserializeVechainTransaction(rawTransaction); + case "stacks": + return deserializeStacksTransaction(rawTransaction); default: { const exhaustiveCheck: never = rawTransaction; // https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking return exhaustiveCheck; diff --git a/packages/core/src/families/stacks/serializer.ts b/packages/core/src/families/stacks/serializer.ts new file mode 100644 index 00000000..a539795e --- /dev/null +++ b/packages/core/src/families/stacks/serializer.ts @@ -0,0 +1,42 @@ +import BigNumber from "bignumber.js"; +import type { RawStacksTransaction, StacksTransaction } from "./types"; + +export const serializeStacksTransaction = ({ + amount, + recipient, + family, + fee, + nonce, + memo, + network, + anchorMode, +}: StacksTransaction): RawStacksTransaction => ({ + amount: amount.toString(), + recipient, + family, + fee: fee?.toString(), + nonce: nonce?.toString(), + memo, + network, + anchorMode, +}); + +export const deserializeStacksTransaction = ({ + amount, + recipient, + family, + fee, + nonce, + memo, + network, + anchorMode, +}: RawStacksTransaction): StacksTransaction => ({ + amount: new BigNumber(amount), + recipient, + family, + fee: fee ? new BigNumber(fee) : undefined, + nonce: nonce ? new BigNumber(nonce) : undefined, + memo, + network: network === "mainnet" ? network : "testnet", + anchorMode, +}); diff --git a/packages/core/src/families/stacks/types.ts b/packages/core/src/families/stacks/types.ts new file mode 100644 index 00000000..3e1c9e25 --- /dev/null +++ b/packages/core/src/families/stacks/types.ts @@ -0,0 +1,18 @@ +import type { z } from "zod"; +import type { TransactionCommon } from "../index"; +import type { schemaRawStacksTransaction } from "./validation"; +import type BigNumber from "bignumber.js"; +import type { AnchorMode } from "@stacks/transactions"; + +export type StacksNetworks = "mainnet" | "testnet"; + +export type StacksTransaction = TransactionCommon & { + readonly family: RawStacksTransaction["family"]; + fee?: BigNumber; + nonce?: BigNumber; + memo?: string; + network: StacksNetworks; + anchorMode: AnchorMode; +}; + +export type RawStacksTransaction = z.infer; diff --git a/packages/core/src/families/stacks/validation.ts b/packages/core/src/families/stacks/validation.ts new file mode 100644 index 00000000..a55632da --- /dev/null +++ b/packages/core/src/families/stacks/validation.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { schemaFamilies, schemaTransactionCommon } from "../common"; + +export const schemaRawStacksTransaction = schemaTransactionCommon.extend({ + family: z.literal(schemaFamilies.enum.stacks), + fee: z.string().optional(), + nonce: z.string().optional(), + memo: z.string().optional(), + network: z.string(), + anchorMode: z.number(), +}); diff --git a/packages/core/src/families/types.ts b/packages/core/src/families/types.ts index 2191e5e0..a5ae9458 100644 --- a/packages/core/src/families/types.ts +++ b/packages/core/src/families/types.ts @@ -21,6 +21,7 @@ import type { TezosTransaction } from "./tezos/types"; import type { TronTransaction } from "./tron/types"; import type { VechainTransaction } from "./vechain/types"; import type { schemaRawTransaction } from "./validation"; +import type { StacksTransaction } from "./stacks/types"; /** * Supported coin families @@ -81,4 +82,5 @@ export type Transaction = | ElrondTransaction | CardanoTransaction | SolanaTransaction - | VechainTransaction; + | VechainTransaction + | StacksTransaction; diff --git a/packages/core/src/families/validation.ts b/packages/core/src/families/validation.ts index 4a9b5417..1ba5ee6f 100644 --- a/packages/core/src/families/validation.ts +++ b/packages/core/src/families/validation.ts @@ -18,6 +18,7 @@ import { schemaRawFilecoinTransaction } from "./filecoin/validation"; import { schemaRawElrondTransaction } from "./elrond/validation"; import { schemaRawCardanoTransaction } from "./cardano/validation"; import { schemaRawVechainTransaction } from "./vechain/validation"; +import { schemaRawStacksTransaction } from "./stacks/validation"; export const schemaRawTransaction = z.discriminatedUnion("family", [ schemaRawAlgorandTransaction, @@ -39,4 +40,5 @@ export const schemaRawTransaction = z.discriminatedUnion("family", [ schemaRawCardanoTransaction, schemaRawSolanaTransaction, schemaRawVechainTransaction, + schemaRawStacksTransaction, ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82a46315..4f2a4c1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,6 +345,9 @@ importers: '@ledgerhq/jest-shared-config': specifier: workspace:* version: link:../jest-shared-config + '@stacks/transactions': + specifier: ^6.13.0 + version: 6.13.0 '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -2259,6 +2262,14 @@ packages: '@lezer/lr': 1.4.0 dev: false + /@noble/hashes@1.1.5: + resolution: {integrity: sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==} + dev: true + + /@noble/secp256k1@1.7.1: + resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2863,6 +2874,35 @@ packages: '@sinonjs/commons': 3.0.1 dev: true + /@stacks/common@6.13.0: + resolution: {integrity: sha512-wwzyihjaSdmL6NxKvDeayy3dqM0L0Q2sawmdNtzJDi0FnXuJGm5PeapJj7bEfcI9XwI7Bw5jZoC6mCn9nc5YIw==} + dependencies: + '@types/bn.js': 5.1.5 + '@types/node': 18.19.29 + dev: true + + /@stacks/network@6.13.0: + resolution: {integrity: sha512-Ss/Da4BNyPBBj1OieM981fJ7SkevKqLPkzoI1+Yo7cYR2df+0FipIN++Z4RfpJpc8ne60vgcx7nJZXQsiGhKBQ==} + dependencies: + '@stacks/common': 6.13.0 + cross-fetch: 3.1.8 + transitivePeerDependencies: + - encoding + dev: true + + /@stacks/transactions@6.13.0: + resolution: {integrity: sha512-xrx09qsXL/tWCkvAArzsFQqtZKDXyedjdVB9uX8xw+cQCi3xZ7r5MHMKzvEsTgJz3EO+MkQBXcvI1uzfuoqhcA==} + dependencies: + '@noble/hashes': 1.1.5 + '@noble/secp256k1': 1.7.1 + '@stacks/common': 6.13.0 + '@stacks/network': 6.13.0 + c32check: 2.0.0 + lodash.clonedeep: 4.5.0 + transitivePeerDependencies: + - encoding + dev: true + /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: @@ -3049,6 +3089,12 @@ packages: '@babel/types': 7.23.9 dev: true + /@types/bn.js@5.1.5: + resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} + dependencies: + '@types/node': 20.11.19 + dev: true + /@types/d3-scale-chromatic@3.0.3: resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} dev: false @@ -3174,6 +3220,12 @@ packages: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true + /@types/node@18.19.29: + resolution: {integrity: sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/node@20.11.19: resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==} dependencies: @@ -3992,6 +4044,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base-x@4.0.0: + resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} + dev: true + /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -4091,6 +4147,14 @@ packages: streamsearch: 1.1.0 dev: false + /c32check@2.0.0: + resolution: {integrity: sha512-rpwfAcS/CMqo0oCqDf3r9eeLgScRE3l/xHDCXhM3UyrfvIn7PrLq63uHh7yYbv8NzaZn5MVsVhIRpQ+5GZ5HyA==} + engines: {node: '>=8'} + dependencies: + '@noble/hashes': 1.1.5 + base-x: 4.0.0 + dev: true + /call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -4407,6 +4471,14 @@ packages: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} dev: false + /cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: true + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -7414,6 +7486,10 @@ packages: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: false + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: true + /lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} dev: false