From 2e1bf3ace7279f7edb959c0c9f503c43ccf551e1 Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:07:29 +0300 Subject: [PATCH 1/7] chore(workspace): move btc package under packages/assets --- packages/{ => assets}/btc/biome.json | 0 packages/{ => assets}/btc/eslint.config.mjs | 0 packages/{ => assets}/btc/package.json | 0 packages/{ => assets}/btc/src/__test__/unit/network.test.ts | 0 .../btc/src/__test__/unit/provider/blockbook.test.ts | 0 packages/{ => assets}/btc/src/__test__/unit/slip132.test.ts | 0 packages/{ => assets}/btc/src/__test__/unit/tx.test.ts | 0 packages/{ => assets}/btc/src/__test__/unit/wallet.test.ts | 0 packages/{ => assets}/btc/src/constants.ts | 0 packages/{ => assets}/btc/src/index.ts | 0 packages/{ => assets}/btc/src/network.ts | 0 packages/{ => assets}/btc/src/provider/blockbook.ts | 0 packages/{ => assets}/btc/src/provider/index.ts | 0 packages/{ => assets}/btc/src/provider/types.ts | 0 packages/{ => assets}/btc/src/slip132.ts | 0 packages/{ => assets}/btc/src/tx.ts | 0 packages/{ => assets}/btc/src/types.ts | 0 packages/{ => assets}/btc/src/wallet.ts | 0 packages/{ => assets}/btc/tsconfig.json | 0 packages/{ => assets}/btc/tsup.config.ts | 0 packages/{ => assets}/btc/vitest.config.ts | 0 pnpm-workspace.yaml | 1 + 22 files changed, 1 insertion(+) rename packages/{ => assets}/btc/biome.json (100%) rename packages/{ => assets}/btc/eslint.config.mjs (100%) rename packages/{ => assets}/btc/package.json (100%) rename packages/{ => assets}/btc/src/__test__/unit/network.test.ts (100%) rename packages/{ => assets}/btc/src/__test__/unit/provider/blockbook.test.ts (100%) rename packages/{ => assets}/btc/src/__test__/unit/slip132.test.ts (100%) rename packages/{ => assets}/btc/src/__test__/unit/tx.test.ts (100%) rename packages/{ => assets}/btc/src/__test__/unit/wallet.test.ts (100%) rename packages/{ => assets}/btc/src/constants.ts (100%) rename packages/{ => assets}/btc/src/index.ts (100%) rename packages/{ => assets}/btc/src/network.ts (100%) rename packages/{ => assets}/btc/src/provider/blockbook.ts (100%) rename packages/{ => assets}/btc/src/provider/index.ts (100%) rename packages/{ => assets}/btc/src/provider/types.ts (100%) rename packages/{ => assets}/btc/src/slip132.ts (100%) rename packages/{ => assets}/btc/src/tx.ts (100%) rename packages/{ => assets}/btc/src/types.ts (100%) rename packages/{ => assets}/btc/src/wallet.ts (100%) rename packages/{ => assets}/btc/tsconfig.json (100%) rename packages/{ => assets}/btc/tsup.config.ts (100%) rename packages/{ => assets}/btc/vitest.config.ts (100%) diff --git a/packages/btc/biome.json b/packages/assets/btc/biome.json similarity index 100% rename from packages/btc/biome.json rename to packages/assets/btc/biome.json diff --git a/packages/btc/eslint.config.mjs b/packages/assets/btc/eslint.config.mjs similarity index 100% rename from packages/btc/eslint.config.mjs rename to packages/assets/btc/eslint.config.mjs diff --git a/packages/btc/package.json b/packages/assets/btc/package.json similarity index 100% rename from packages/btc/package.json rename to packages/assets/btc/package.json diff --git a/packages/btc/src/__test__/unit/network.test.ts b/packages/assets/btc/src/__test__/unit/network.test.ts similarity index 100% rename from packages/btc/src/__test__/unit/network.test.ts rename to packages/assets/btc/src/__test__/unit/network.test.ts diff --git a/packages/btc/src/__test__/unit/provider/blockbook.test.ts b/packages/assets/btc/src/__test__/unit/provider/blockbook.test.ts similarity index 100% rename from packages/btc/src/__test__/unit/provider/blockbook.test.ts rename to packages/assets/btc/src/__test__/unit/provider/blockbook.test.ts diff --git a/packages/btc/src/__test__/unit/slip132.test.ts b/packages/assets/btc/src/__test__/unit/slip132.test.ts similarity index 100% rename from packages/btc/src/__test__/unit/slip132.test.ts rename to packages/assets/btc/src/__test__/unit/slip132.test.ts diff --git a/packages/btc/src/__test__/unit/tx.test.ts b/packages/assets/btc/src/__test__/unit/tx.test.ts similarity index 100% rename from packages/btc/src/__test__/unit/tx.test.ts rename to packages/assets/btc/src/__test__/unit/tx.test.ts diff --git a/packages/btc/src/__test__/unit/wallet.test.ts b/packages/assets/btc/src/__test__/unit/wallet.test.ts similarity index 100% rename from packages/btc/src/__test__/unit/wallet.test.ts rename to packages/assets/btc/src/__test__/unit/wallet.test.ts diff --git a/packages/btc/src/constants.ts b/packages/assets/btc/src/constants.ts similarity index 100% rename from packages/btc/src/constants.ts rename to packages/assets/btc/src/constants.ts diff --git a/packages/btc/src/index.ts b/packages/assets/btc/src/index.ts similarity index 100% rename from packages/btc/src/index.ts rename to packages/assets/btc/src/index.ts diff --git a/packages/btc/src/network.ts b/packages/assets/btc/src/network.ts similarity index 100% rename from packages/btc/src/network.ts rename to packages/assets/btc/src/network.ts diff --git a/packages/btc/src/provider/blockbook.ts b/packages/assets/btc/src/provider/blockbook.ts similarity index 100% rename from packages/btc/src/provider/blockbook.ts rename to packages/assets/btc/src/provider/blockbook.ts diff --git a/packages/btc/src/provider/index.ts b/packages/assets/btc/src/provider/index.ts similarity index 100% rename from packages/btc/src/provider/index.ts rename to packages/assets/btc/src/provider/index.ts diff --git a/packages/btc/src/provider/types.ts b/packages/assets/btc/src/provider/types.ts similarity index 100% rename from packages/btc/src/provider/types.ts rename to packages/assets/btc/src/provider/types.ts diff --git a/packages/btc/src/slip132.ts b/packages/assets/btc/src/slip132.ts similarity index 100% rename from packages/btc/src/slip132.ts rename to packages/assets/btc/src/slip132.ts diff --git a/packages/btc/src/tx.ts b/packages/assets/btc/src/tx.ts similarity index 100% rename from packages/btc/src/tx.ts rename to packages/assets/btc/src/tx.ts diff --git a/packages/btc/src/types.ts b/packages/assets/btc/src/types.ts similarity index 100% rename from packages/btc/src/types.ts rename to packages/assets/btc/src/types.ts diff --git a/packages/btc/src/wallet.ts b/packages/assets/btc/src/wallet.ts similarity index 100% rename from packages/btc/src/wallet.ts rename to packages/assets/btc/src/wallet.ts diff --git a/packages/btc/tsconfig.json b/packages/assets/btc/tsconfig.json similarity index 100% rename from packages/btc/tsconfig.json rename to packages/assets/btc/tsconfig.json diff --git a/packages/btc/tsup.config.ts b/packages/assets/btc/tsup.config.ts similarity index 100% rename from packages/btc/tsup.config.ts rename to packages/assets/btc/tsup.config.ts diff --git a/packages/btc/vitest.config.ts b/packages/assets/btc/vitest.config.ts similarity index 100% rename from packages/btc/vitest.config.ts rename to packages/assets/btc/vitest.config.ts diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 18ec407e..9ae535fa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - 'packages/*' + - 'packages/assets/*' From 72a91cf5ff42ea0511b475478057d62488343f29 Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:07:41 +0300 Subject: [PATCH 2/7] feat(asset-core): add shared asset interfaces --- packages/assets/asset-core/biome.json | 8 ++ packages/assets/asset-core/package.json | 41 +++++++++ packages/assets/asset-core/src/index.ts | 102 ++++++++++++++++++++++ packages/assets/asset-core/tsconfig.json | 24 +++++ packages/assets/asset-core/tsup.config.ts | 31 +++++++ 5 files changed, 206 insertions(+) create mode 100644 packages/assets/asset-core/biome.json create mode 100644 packages/assets/asset-core/package.json create mode 100644 packages/assets/asset-core/src/index.ts create mode 100644 packages/assets/asset-core/tsconfig.json create mode 100644 packages/assets/asset-core/tsup.config.ts diff --git a/packages/assets/asset-core/biome.json b/packages/assets/asset-core/biome.json new file mode 100644 index 00000000..c365284a --- /dev/null +++ b/packages/assets/asset-core/biome.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "extends": ["../../biome.json"], + "files": { + "include": ["src/**/*.ts"], + "ignore": ["**/dist/**", "**/node_modules/**"] + } +} diff --git a/packages/assets/asset-core/package.json b/packages/assets/asset-core/package.json new file mode 100644 index 00000000..90e25b4e --- /dev/null +++ b/packages/assets/asset-core/package.json @@ -0,0 +1,41 @@ +{ + "name": "@gridplus/asset-core", + "version": "0.1.0", + "type": "module", + "description": "Shared asset interface types for GridPlus SDK", + "scripts": { + "build": "tsup", + "lint": "biome check src", + "lint:fix": "biome check --write src", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/GridPlus/gridplus-sdk.git", + "directory": "packages/assets/asset-core" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^24.10.4", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + }, + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/packages/assets/asset-core/src/index.ts b/packages/assets/asset-core/src/index.ts new file mode 100644 index 00000000..10827973 --- /dev/null +++ b/packages/assets/asset-core/src/index.ts @@ -0,0 +1,102 @@ +export type DerivationPath = number[]; +export type Address = string; +export type PublicKey = Uint8Array; + +export type Signature = { + bytes: Uint8Array; + r?: Uint8Array; + s?: Uint8Array; + v?: number | bigint; +}; + +export type SignResult = { + signature: Signature; + publicKey?: PublicKey; + signedPayload?: Uint8Array | string; + txHash?: string; + metadata?: Record; +}; + +export type Account = { + address: Address; + publicKey?: PublicKey; + path: DerivationPath; + index?: number; +}; + +export type AssetCapabilities = { + signTransaction: boolean; + signMessage: boolean; + signTypedData: boolean; + signArbitrary: boolean; + getPublicKey: boolean; + getXpub?: boolean; +}; + +export type GetAddressParams = { + path?: DerivationPath; + accountIndex?: number; + change?: number; + addressIndex?: number; +}; + +export type GetPublicKeyParams = { + path?: DerivationPath; + accountIndex?: number; + change?: number; + addressIndex?: number; + compressed?: boolean; +}; + +export type GetAccountsParams = { + startIndex?: number; + count?: number; + includePublicKey?: boolean; + change?: number; +}; + +export type SignRequest = + | { kind: 'transaction'; payload: unknown; options?: unknown } + | { kind: 'message'; payload: string | Uint8Array; options?: unknown } + | { kind: 'typedData'; payload: unknown; options?: unknown } + | { kind: 'arbitrary'; payload: Uint8Array; options?: unknown }; + +export type Signer = { + getAddress: (path: DerivationPath, options?: unknown) => Promise
; + getPublicKey: (path: DerivationPath, options?: unknown) => Promise; + sign: (request: TSignRequest) => Promise; +}; + +export type AssetAdapter< + TSignRequest = SignRequest, + TGetAddressParams = GetAddressParams, + TGetAccountsParams = GetAccountsParams, + TGetPublicKeyParams = GetPublicKeyParams, + TAccount = Account, +> = { + getAddress(params?: TGetAddressParams): Promise
; + getAddresses(params?: TGetAccountsParams): Promise; + getPublicKey(params?: TGetPublicKeyParams): Promise; + getPublicKeys?(params?: TGetAccountsParams): Promise; + getAccount?(params?: TGetAddressParams): Promise; + getAccounts?(params?: TGetAccountsParams): Promise; + sign(request: TSignRequest): Promise; + validateAddress?(address: Address): boolean; + normalizeAddress?(address: Address): Address; +}; + +export type AssetModule< + TSignRequest = SignRequest, + TAdapter extends AssetAdapter = AssetAdapter, + TOptions = unknown, + TSigner extends Signer = Signer, +> = { + readonly id: string; + readonly name: string; + readonly coinType: number; + readonly curve: 'secp256k1' | 'ed25519' | 'bls12-381'; + readonly defaultPath: DerivationPath; + readonly supports: AssetCapabilities; + create: (signer: TSigner, options?: TOptions) => TAdapter; + utils: Record; +}; diff --git a/packages/assets/asset-core/tsconfig.json b/packages/assets/asset-core/tsconfig.json new file mode 100644 index 00000000..d8636539 --- /dev/null +++ b/packages/assets/asset-core/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2022", "DOM"], + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./src", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "exclude": ["node_modules"], + "include": ["src"] +} diff --git a/packages/assets/asset-core/tsup.config.ts b/packages/assets/asset-core/tsup.config.ts new file mode 100644 index 00000000..12a41f8d --- /dev/null +++ b/packages/assets/asset-core/tsup.config.ts @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'tsup'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse( + readFileSync(resolve(__dirname, 'package.json'), 'utf-8'), +); + +const external = Object.keys({ + ...(pkg.dependencies ?? {}), + ...(pkg.peerDependencies ?? {}), +}); + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: './dist', + format: ['esm', 'cjs'], + target: 'node20', + sourcemap: true, + clean: true, + bundle: true, + dts: true, + silent: true, + outExtension: ({ format }) => ({ + js: format === 'esm' ? '.mjs' : '.cjs', + }), + external, + tsconfig: './tsconfig.json', +}); From 1920637753d09b59fc2ff26ebd92555cedba38e7 Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:07:51 +0300 Subject: [PATCH 3/7] feat(btc): expose btc AssetModule and signer types --- packages/assets/btc/package.json | 3 +- packages/assets/btc/src/asset.ts | 294 +++++++++++++++++++++++++++++++ packages/assets/btc/src/index.ts | 25 +++ packages/assets/btc/src/types.ts | 10 ++ 4 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 packages/assets/btc/src/asset.ts diff --git a/packages/assets/btc/package.json b/packages/assets/btc/package.json index aa25d38e..e7b17193 100644 --- a/packages/assets/btc/package.json +++ b/packages/assets/btc/package.json @@ -27,9 +27,10 @@ "repository": { "type": "git", "url": "https://github.com/GridPlus/gridplus-sdk.git", - "directory": "packages/btc" + "directory": "packages/assets/btc" }, "dependencies": { + "@gridplus/asset-core": "workspace:*", "bs58check": "^4.0.0" }, "devDependencies": { diff --git a/packages/assets/btc/src/asset.ts b/packages/assets/btc/src/asset.ts new file mode 100644 index 00000000..03df5e94 --- /dev/null +++ b/packages/assets/btc/src/asset.ts @@ -0,0 +1,294 @@ +import { + BTC_COIN_TYPES, + BTC_PURPOSES, + HARDENED_OFFSET, +} from './constants'; +import type { + BtcAddressFormat, + BtcCoinType, + BtcPurpose, + BitcoinSignPayload, + XpubOptions, + XpubsOptions, +} from './types'; +import type { + Account, + Address, + AssetAdapter, + AssetModule, + DerivationPath, + GetAccountsParams, + GetAddressParams, + GetPublicKeyParams, + PublicKey, + Signer as CoreSigner, +} from '@gridplus/asset-core'; + +export type BtcSignRequest = { + kind: 'transaction'; + payload: BitcoinSignPayload; + options?: { format?: BtcAddressFormat }; +}; + +export type BtcGetAddressParams = GetAddressParams & { + includePublicKey?: boolean; + format?: BtcAddressFormat; + purpose?: BtcPurpose; + coinType?: BtcCoinType; +}; + +export type BtcGetAccountsParams = GetAccountsParams & { + format?: BtcAddressFormat; + purpose?: BtcPurpose; + coinType?: BtcCoinType; +}; + +export type BtcGetPublicKeyParams = GetPublicKeyParams & { + purpose?: BtcPurpose; + coinType?: BtcCoinType; +}; + +export type Signer = CoreSigner & { + getXpub?: (options: XpubOptions) => Promise; + getXpubs?: (options: XpubsOptions) => Promise>; + getAllXpubs?: ( + coinType?: BtcCoinType, + account?: number, + ) => Promise<{ xpub: string; ypub: string; zpub: string }>; +}; + +export type BtcAdapter = AssetAdapter< + BtcSignRequest, + BtcGetAddressParams, + BtcGetAccountsParams, + BtcGetPublicKeyParams, + Account +> & { + getXpub?(options: XpubOptions): Promise; + getXpubs?(options: XpubsOptions): Promise>; + getAllXpubs?( + coinType?: BtcCoinType, + account?: number, + ): Promise<{ xpub: string; ypub: string; zpub: string }>; +}; + +export type BtcAdapterOptions = { + purpose?: BtcPurpose; + coinType?: BtcCoinType; + accountIndex?: number; + change?: number; + format?: BtcAddressFormat; +}; + +const DEFAULT_PURPOSE: BtcPurpose = BTC_PURPOSES.NATIVE; +const DEFAULT_COIN_TYPE: BtcCoinType = BTC_COIN_TYPES.MAINNET; +const DEFAULT_ACCOUNT = 0; +const DEFAULT_CHANGE = 0; +const DEFAULT_ADDRESS_INDEX = 0; + +const buildPath = ( + purpose: BtcPurpose, + coinType: BtcCoinType, + accountIndex: number, + change: number, + addressIndex: number, +): DerivationPath => { + return [ + purpose + HARDENED_OFFSET, + coinType + HARDENED_OFFSET, + accountIndex + HARDENED_OFFSET, + change, + addressIndex, + ]; +}; + +const inferFormatFromPurpose = (purpose: BtcPurpose): BtcAddressFormat => { + if (purpose === BTC_PURPOSES.LEGACY) return 'legacy'; + if (purpose === BTC_PURPOSES.WRAPPED) return 'wrapped'; + return 'native'; +}; + +const resolvePurpose = ( + params?: BtcGetAddressParams, + options?: BtcAdapterOptions, +): BtcPurpose => params?.purpose ?? options?.purpose ?? DEFAULT_PURPOSE; + +const resolveCoinType = ( + params?: BtcGetAddressParams, + options?: BtcAdapterOptions, +): BtcCoinType => params?.coinType ?? options?.coinType ?? DEFAULT_COIN_TYPE; + +const resolveFormat = ( + purpose: BtcPurpose, + params?: { format?: BtcAddressFormat }, + options?: BtcAdapterOptions, +): BtcAddressFormat => + params?.format ?? options?.format ?? inferFormatFromPurpose(purpose); + +const resolveAccountIndex = ( + params?: BtcGetAddressParams, + options?: BtcAdapterOptions, +): number => params?.accountIndex ?? options?.accountIndex ?? DEFAULT_ACCOUNT; + +const resolveChange = ( + params?: { change?: number }, + options?: BtcAdapterOptions, +): number => params?.change ?? options?.change ?? DEFAULT_CHANGE; + +const resolveAddressIndex = (params?: { addressIndex?: number }): number => + params?.addressIndex ?? DEFAULT_ADDRESS_INDEX; + +const resolvePath = ( + params?: BtcGetAddressParams, + options?: BtcAdapterOptions, +): DerivationPath => { + return buildPath(resolvePurpose(params, options), resolveCoinType(params, options), resolveAccountIndex(params, options), resolveChange(params, options), resolveAddressIndex(params)); +}; + +export const btc: AssetModule< + BtcSignRequest, + BtcAdapter, + BtcAdapterOptions, + Signer +> = { + id: 'btc', + name: 'Bitcoin', + coinType: BTC_COIN_TYPES.MAINNET, + curve: 'secp256k1', + defaultPath: buildPath( + DEFAULT_PURPOSE, + DEFAULT_COIN_TYPE, + DEFAULT_ACCOUNT, + DEFAULT_CHANGE, + DEFAULT_ADDRESS_INDEX, + ), + supports: { + signTransaction: true, + signMessage: false, + signTypedData: false, + signArbitrary: false, + getPublicKey: true, + getXpub: true, + }, + create: (signer: Signer, options: BtcAdapterOptions = {}): BtcAdapter => { + const getAddress = async (params: BtcGetAddressParams = {}): Promise
=> { + const path = resolvePath(params, options); + return signer.getAddress(path, { format: params.format }); + }; + + const getPublicKey = async ( + params: BtcGetPublicKeyParams = {}, + ): Promise => { + const path = resolvePath(params, options); + return signer.getPublicKey(path, { compressed: true }); + }; + + const getAddresses = async ( + params: BtcGetAccountsParams = {}, + ): Promise => { + const purpose = resolvePurpose(params, options); + const format = resolveFormat(purpose, params, options); + const startIndex = params.startIndex ?? 0; + const addresses: Address[] = []; + for (let i = 0; i < (params.count ?? 1); i += 1) { + const path = resolvePath({ ...params, addressIndex: startIndex + i, purpose, format }, options); + addresses.push(await getAddress({ ...params, path, format })); + } + return addresses; + }; + + const getAccount = async ( + params: BtcGetAddressParams = {}, + ): Promise => { + const address = await getAddress(params); + const publicKey = params.includePublicKey + ? await getPublicKey(params) + : undefined; + const purpose = resolvePurpose(params, options); + const coinType = resolveCoinType(params, options); + const accountIndex = resolveAccountIndex(params, options); + const change = resolveChange(params, options); + const addressIndex = resolveAddressIndex(params); + const path = + params.path ?? + buildPath(purpose, coinType, accountIndex, change, addressIndex); + return { + address, + publicKey, + path, + index: addressIndex, + }; + }; + + const getAccounts = async ( + params: BtcGetAccountsParams = {}, + ): Promise => { + const purpose = resolvePurpose(params, options); + const coinType = resolveCoinType(params, options); + const format = resolveFormat(purpose, params, options); + const accountIndex = resolveAccountIndex(params, options); + const change = resolveChange(params, options); + const startIndex = params.startIndex ?? 0; + const count = params.count ?? 1; + const accounts: Account[] = []; + for (let i = 0; i < count; i += 1) { + const addressIndex = startIndex + i; + const path = buildPath( + purpose, + coinType, + accountIndex, + change, + addressIndex, + ); + const address = await signer.getAddress(path, { format }); + const publicKey = params.includePublicKey + ? await signer.getPublicKey(path, { compressed: true }) + : undefined; + accounts.push({ + address, + publicKey, + path, + index: addressIndex, + }); + } + return accounts; + }; + + const getXpub = async (opts: XpubOptions) => { + if (!signer.getXpub) { + throw new Error('Signer does not support getXpub'); + } + return signer.getXpub(opts); + }; + + const getXpubs = async (opts: XpubsOptions) => { + if (!signer.getXpubs) { + throw new Error('Signer does not support getXpubs'); + } + return signer.getXpubs(opts); + }; + + const getAllXpubs = async (coinType?: BtcCoinType, account?: number) => { + if (!signer.getAllXpubs) { + throw new Error('Signer does not support getAllXpubs'); + } + return signer.getAllXpubs(coinType, account); + }; + + return { + getAddress, + getAddresses, + getPublicKey, + getAccount, + getAccounts, + sign: (request) => signer.sign(request), + getXpub: signer.getXpub ? getXpub : undefined, + getXpubs: signer.getXpubs ? getXpubs : undefined, + getAllXpubs: signer.getAllXpubs ? getAllXpubs : undefined, + }; + }, + utils: { + buildPath, + inferFormatFromPurpose, + }, +}; diff --git a/packages/assets/btc/src/index.ts b/packages/assets/btc/src/index.ts index efb3db9a..4aa57649 100644 --- a/packages/assets/btc/src/index.ts +++ b/packages/assets/btc/src/index.ts @@ -14,9 +14,11 @@ export type { BtcNetwork, XpubPrefix, ScriptType, + BtcAddressFormat, XpubOptions, XpubsOptions, PreviousOutput, + BitcoinSignPayload, WalletUtxo, WalletSummary, WalletSnapshot, @@ -65,3 +67,26 @@ export type { FeeRates, PagingOptions, } from './provider/types'; + +// Asset module +export { btc } from './asset'; +export type { + AssetCapabilities, + AssetModule, + Account, + GetAccountsParams, + GetAddressParams, + GetPublicKeyParams, + SignRequest, + SignResult, +} from '@gridplus/asset-core'; + +export type { + BtcAdapter, + BtcAdapterOptions, + BtcGetAccountsParams, + BtcGetAddressParams, + BtcGetPublicKeyParams, + BtcSignRequest, + Signer, +} from './asset'; diff --git a/packages/assets/btc/src/types.ts b/packages/assets/btc/src/types.ts index c3395451..0b7c9f1a 100644 --- a/packages/assets/btc/src/types.ts +++ b/packages/assets/btc/src/types.ts @@ -3,6 +3,7 @@ export type BtcCoinType = 0 | 1; export type BtcNetwork = 'mainnet' | 'testnet' | 'regtest'; export type XpubPrefix = 'xpub' | 'ypub' | 'zpub' | 'tpub' | 'upub' | 'vpub'; export type ScriptType = 'p2pkh' | 'p2sh-p2wpkh' | 'p2wpkh'; +export type BtcAddressFormat = 'legacy' | 'wrapped' | 'native'; export interface XpubOptions { purpose: BtcPurpose; @@ -24,6 +25,15 @@ export interface PreviousOutput { signerPath: number[]; } +/** Transaction payload for device signing */ +export interface BitcoinSignPayload { + prevOuts: PreviousOutput[]; + recipient: string; + value: number; + fee: number; + changePath: number[]; +} + /** Wallet UTXO with derivation info */ export interface WalletUtxo { txid: string; From 50c584a4ef21936ad24aef56e08646c105176d2b Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:07:56 +0300 Subject: [PATCH 4/7] feat(assets): add cosmos and evm asset modules --- packages/assets/cosmos/biome.json | 8 + packages/assets/cosmos/package.json | 46 ++++++ packages/assets/cosmos/src/asset.ts | 213 ++++++++++++++++++++++++ packages/assets/cosmos/src/index.ts | 11 ++ packages/assets/cosmos/tsconfig.json | 24 +++ packages/assets/cosmos/tsup.config.ts | 31 ++++ packages/assets/evm/biome.json | 8 + packages/assets/evm/package.json | 45 ++++++ packages/assets/evm/src/asset.ts | 224 ++++++++++++++++++++++++++ packages/assets/evm/src/index.ts | 12 ++ packages/assets/evm/tsconfig.json | 24 +++ packages/assets/evm/tsup.config.ts | 31 ++++ 12 files changed, 677 insertions(+) create mode 100644 packages/assets/cosmos/biome.json create mode 100644 packages/assets/cosmos/package.json create mode 100644 packages/assets/cosmos/src/asset.ts create mode 100644 packages/assets/cosmos/src/index.ts create mode 100644 packages/assets/cosmos/tsconfig.json create mode 100644 packages/assets/cosmos/tsup.config.ts create mode 100644 packages/assets/evm/biome.json create mode 100644 packages/assets/evm/package.json create mode 100644 packages/assets/evm/src/asset.ts create mode 100644 packages/assets/evm/src/index.ts create mode 100644 packages/assets/evm/tsconfig.json create mode 100644 packages/assets/evm/tsup.config.ts diff --git a/packages/assets/cosmos/biome.json b/packages/assets/cosmos/biome.json new file mode 100644 index 00000000..c365284a --- /dev/null +++ b/packages/assets/cosmos/biome.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "extends": ["../../biome.json"], + "files": { + "include": ["src/**/*.ts"], + "ignore": ["**/dist/**", "**/node_modules/**"] + } +} diff --git a/packages/assets/cosmos/package.json b/packages/assets/cosmos/package.json new file mode 100644 index 00000000..30da4ef8 --- /dev/null +++ b/packages/assets/cosmos/package.json @@ -0,0 +1,46 @@ +{ + "name": "@gridplus/cosmos", + "version": "0.1.0", + "type": "module", + "description": "Cosmos-family asset interface for GridPlus SDK", + "scripts": { + "build": "tsup", + "lint": "biome check src", + "lint:fix": "biome check --write src", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/GridPlus/gridplus-sdk.git", + "directory": "packages/assets/cosmos" + }, + "dependencies": { + "@gridplus/asset-core": "workspace:*", + "@noble/hashes": "^1.8.0", + "bech32": "^2.0.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^24.10.4", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + }, + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/packages/assets/cosmos/src/asset.ts b/packages/assets/cosmos/src/asset.ts new file mode 100644 index 00000000..00cc3b76 --- /dev/null +++ b/packages/assets/cosmos/src/asset.ts @@ -0,0 +1,213 @@ +import type { + Account, + Address, + AssetAdapter, + AssetModule, + DerivationPath, + GetAccountsParams, + GetAddressParams, + GetPublicKeyParams, + PublicKey, + SignResult, + Signer as CoreSigner, +} from '@gridplus/asset-core'; +import { bech32 } from 'bech32'; +import { ripemd160 } from '@noble/hashes/ripemd160'; +import { sha256 } from '@noble/hashes/sha256'; + +export type CosmosSignMode = 'direct' | 'amino'; + +export type CosmosSignRequest = { + kind: 'transaction'; + payload: Uint8Array | Buffer; + options?: { path?: DerivationPath; mode?: CosmosSignMode }; +}; + +export type Signer = CoreSigner; + +export type CosmosGetAddressParams = GetAddressParams & { + hrp?: string; + includePublicKey?: boolean; +}; + +export type CosmosGetPublicKeyParams = GetPublicKeyParams & { + /** Cosmos public keys are typically compressed secp256k1 (33 bytes). */ + compressed?: boolean; +}; + +export type CosmosAdapterOptions = { + /** Default derivation params used when no explicit path is provided. */ + coinType?: number; + accountIndex?: number; + change?: number; + addressIndex?: number; + hrp?: string; +}; + +export type CosmosAdapter = AssetAdapter< + CosmosSignRequest, + CosmosGetAddressParams, + GetAccountsParams, + CosmosGetPublicKeyParams, + Account +> & { + signDirect?: ( + signDoc: Uint8Array | Buffer, + options?: { path?: DerivationPath }, + ) => Promise; + signAmino?: ( + aminoSignDoc: Uint8Array | Buffer, + options?: { path?: DerivationPath }, + ) => Promise; +}; + +const HARDENED_OFFSET = 0x80000000; + +const DEFAULT_COIN_TYPE = 118; +const DEFAULT_HRP = 'cosmos'; + +const buildPath = ( + coinType: number, + accountIndex: number, + change: number, + addressIndex: number, +): DerivationPath => { + return [ + 44 + HARDENED_OFFSET, + coinType + HARDENED_OFFSET, + accountIndex + HARDENED_OFFSET, + change, + addressIndex, + ]; +}; + +function compressSecp256k1Pubkey(pubkey: Uint8Array): Uint8Array { + if (pubkey.length === 33 && (pubkey[0] === 0x02 || pubkey[0] === 0x03)) { + return pubkey; + } + if (pubkey.length === 65 && pubkey[0] === 0x04) { + const x = pubkey.slice(1, 33); + const yLastByte = pubkey[64]; + const prefix = yLastByte % 2 === 0 ? 0x02 : 0x03; + const out = new Uint8Array(33); + out[0] = prefix; + out.set(x, 1); + return out; + } + // Unknown format, return as-is. + return pubkey; +} + +const pubkeyToBech32Address = (pubkeyCompressed: Uint8Array, hrp: string) => { + const digest = ripemd160(sha256(pubkeyCompressed)); + const words = bech32.toWords(digest); + return bech32.encode(hrp, words); +}; + +const resolvePath = ( + params?: { path?: DerivationPath; accountIndex?: number; change?: number; addressIndex?: number }, + options?: CosmosAdapterOptions, +): DerivationPath => { + if (params?.path) return params.path; + const coinType = options?.coinType ?? DEFAULT_COIN_TYPE; + const accountIndex = params?.accountIndex ?? options?.accountIndex ?? 0; + const change = params?.change ?? options?.change ?? 0; + const addressIndex = params?.addressIndex ?? options?.addressIndex ?? 0; + return buildPath(coinType, accountIndex, change, addressIndex); +}; + +export const cosmos: AssetModule< + CosmosSignRequest, + CosmosAdapter, + CosmosAdapterOptions +> = { + id: 'cosmos', + name: 'Cosmos', + coinType: DEFAULT_COIN_TYPE, + curve: 'secp256k1', + defaultPath: buildPath(DEFAULT_COIN_TYPE, 0, 0, 0), + supports: { + signTransaction: true, + signMessage: false, + signTypedData: false, + signArbitrary: false, + getPublicKey: true, + }, + create: (signer: Signer, options?: CosmosAdapterOptions): CosmosAdapter => { + const getPublicKey = async (params: CosmosGetPublicKeyParams = {}) : Promise => { + const path = resolvePath(params, options); + const wantCompressed = params.compressed ?? true; + const pk = await signer.getPublicKey(path, { compressed: wantCompressed }); + return wantCompressed ? compressSecp256k1Pubkey(pk) : pk; + }; + + const getAddress = async (params: CosmosGetAddressParams = {}) => { + const hrp = params.hrp ?? options?.hrp ?? DEFAULT_HRP; + const pubkey = await getPublicKey({ + ...params, + compressed: true, + }); + return pubkeyToBech32Address(pubkey, hrp); + }; + + const getAddresses = async (params: GetAccountsParams = {}) => { + const hrp = options?.hrp ?? DEFAULT_HRP; + const startIndex = params.startIndex ?? 0; + const count = params.count ?? 1; + const addresses: Address[] = []; + for (let i = 0; i < count; i += 1) { + addresses.push( + await getAddress({ + hrp, + addressIndex: startIndex + i, + change: params.change, + }), + ); + } + return addresses; + }; + + const getAccount = async (params: CosmosGetAddressParams = {}) => { + const address = await getAddress(params); + const path = resolvePath(params, options); + const publicKey = params.includePublicKey + ? await getPublicKey({ ...params, compressed: true }) + : undefined; + return { address, publicKey, path, index: params.addressIndex }; + }; + + const sign = async (request: CosmosSignRequest): Promise => { + const path = request.options?.path ?? resolvePath(undefined, options); + const next: CosmosSignRequest = { + ...request, + options: { ...(request.options ?? {}), path }, + }; + return signer.sign(next); + }; + + return { + getAddress, + getAddresses, + getPublicKey, + getAccount, + sign, + signDirect: (signDoc, signOptions) => + sign({ + kind: 'transaction', + payload: signDoc, + options: { ...(signOptions ?? {}), mode: 'direct' }, + }), + signAmino: (aminoSignDoc, signOptions) => + sign({ + kind: 'transaction', + payload: aminoSignDoc, + options: { ...(signOptions ?? {}), mode: 'amino' }, + }), + }; + }, + utils: { + buildPath, + compressSecp256k1Pubkey, + pubkeyToBech32Address, + }, +}; diff --git a/packages/assets/cosmos/src/index.ts b/packages/assets/cosmos/src/index.ts new file mode 100644 index 00000000..a75c7fc6 --- /dev/null +++ b/packages/assets/cosmos/src/index.ts @@ -0,0 +1,11 @@ +export { cosmos } from './asset'; + +export type { + CosmosAdapter, + CosmosAdapterOptions, + CosmosGetAddressParams, + CosmosGetPublicKeyParams, + CosmosSignMode, + CosmosSignRequest, + Signer, +} from './asset'; diff --git a/packages/assets/cosmos/tsconfig.json b/packages/assets/cosmos/tsconfig.json new file mode 100644 index 00000000..d8636539 --- /dev/null +++ b/packages/assets/cosmos/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2022", "DOM"], + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./src", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "exclude": ["node_modules"], + "include": ["src"] +} diff --git a/packages/assets/cosmos/tsup.config.ts b/packages/assets/cosmos/tsup.config.ts new file mode 100644 index 00000000..12a41f8d --- /dev/null +++ b/packages/assets/cosmos/tsup.config.ts @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'tsup'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse( + readFileSync(resolve(__dirname, 'package.json'), 'utf-8'), +); + +const external = Object.keys({ + ...(pkg.dependencies ?? {}), + ...(pkg.peerDependencies ?? {}), +}); + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: './dist', + format: ['esm', 'cjs'], + target: 'node20', + sourcemap: true, + clean: true, + bundle: true, + dts: true, + silent: true, + outExtension: ({ format }) => ({ + js: format === 'esm' ? '.mjs' : '.cjs', + }), + external, + tsconfig: './tsconfig.json', +}); diff --git a/packages/assets/evm/biome.json b/packages/assets/evm/biome.json new file mode 100644 index 00000000..c365284a --- /dev/null +++ b/packages/assets/evm/biome.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "extends": ["../../biome.json"], + "files": { + "include": ["src/**/*.ts"], + "ignore": ["**/dist/**", "**/node_modules/**"] + } +} diff --git a/packages/assets/evm/package.json b/packages/assets/evm/package.json new file mode 100644 index 00000000..45adbc90 --- /dev/null +++ b/packages/assets/evm/package.json @@ -0,0 +1,45 @@ +{ + "name": "@gridplus/evm", + "version": "0.1.0", + "type": "module", + "description": "EVM asset interface for GridPlus SDK", + "scripts": { + "build": "tsup", + "lint": "biome check src", + "lint:fix": "biome check --write src", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/GridPlus/gridplus-sdk.git", + "directory": "packages/assets/evm" + }, + "dependencies": { + "@gridplus/asset-core": "workspace:*", + "viem": "^2.37.8" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^24.10.4", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + }, + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/packages/assets/evm/src/asset.ts b/packages/assets/evm/src/asset.ts new file mode 100644 index 00000000..95cdc6b6 --- /dev/null +++ b/packages/assets/evm/src/asset.ts @@ -0,0 +1,224 @@ +import type { + Account, + Address, + AssetAdapter, + AssetModule, + DerivationPath, + GetAccountsParams, + GetAddressParams, + GetPublicKeyParams, + PublicKey, + SignResult, + Signer as CoreSigner, +} from '@gridplus/asset-core'; +import { getAddress as checksumAddress, isAddress } from 'viem'; +import type { Hex, TransactionSerializable } from 'viem'; + +export type Eip712Payload = { + types: Record>; + domain: Record; + primaryType: string; + message: Record; +}; + +export type EvmRawTransaction = Hex | Uint8Array | Buffer; + +export type EvmSignRequest = + | { + kind: 'transaction'; + payload: TransactionSerializable | EvmRawTransaction; + options?: { path?: DerivationPath }; + } + | { + kind: 'message'; + payload: string | Uint8Array | Buffer; + options?: { path?: DerivationPath; protocol?: 'signPersonal' }; + } + | { + kind: 'typedData'; + payload: Eip712Payload; + options?: { path?: DerivationPath }; + }; + +export type Signer = CoreSigner; + +export type EvmGetAddressParams = GetAddressParams & { + checksum?: boolean; + includePublicKey?: boolean; +}; + +export type EvmGetPublicKeyParams = GetPublicKeyParams; + +export type EvmAdapterOptions = { + /** Default path used for getAddress/getPublicKey/sign if none is provided. */ + path?: DerivationPath; + accountIndex?: number; + change?: number; + addressIndex?: number; + checksum?: boolean; +}; + +export type EvmAdapter = AssetAdapter< + EvmSignRequest, + EvmGetAddressParams, + GetAccountsParams, + EvmGetPublicKeyParams, + Account +> & { + signTransaction?: ( + tx: TransactionSerializable | EvmRawTransaction, + options?: { path?: DerivationPath }, + ) => Promise; + signMessage?: ( + msg: string | Uint8Array | Buffer, + options?: { path?: DerivationPath; protocol?: 'signPersonal' }, + ) => Promise; + signTypedData?: ( + typedData: Eip712Payload, + options?: { path?: DerivationPath }, + ) => Promise; +}; + +const HARDENED_OFFSET = 0x80000000; +const DEFAULT_PATH: DerivationPath = [ + 44 + HARDENED_OFFSET, + 60 + HARDENED_OFFSET, + 0 + HARDENED_OFFSET, + 0, + 0, +]; + +const buildPath = ( + accountIndex: number, + change: number, + addressIndex: number, +): DerivationPath => { + return [ + 44 + HARDENED_OFFSET, + 60 + HARDENED_OFFSET, + accountIndex + HARDENED_OFFSET, + change, + addressIndex, + ]; +}; + +const resolvePath = ( + params?: { + path?: DerivationPath; + accountIndex?: number; + change?: number; + addressIndex?: number; + }, + options?: EvmAdapterOptions, +): DerivationPath => { + if (params?.path) return params.path; + + const base = options?.path ? [...options.path] : [...DEFAULT_PATH]; + + const defaultAccount = + options?.path && base.length >= 3 ? base[2] - HARDENED_OFFSET : 0; + const defaultChange = options?.path && base.length >= 4 ? base[3] : 0; + const defaultIndex = options?.path && base.length >= 5 ? base[4] : 0; + + const accountIndex = + params?.accountIndex ?? options?.accountIndex ?? defaultAccount; + const change = params?.change ?? options?.change ?? defaultChange; + const addressIndex = + params?.addressIndex ?? options?.addressIndex ?? defaultIndex; + + // Ensure 5-depth path. + if (base.length < 5) { + return buildPath(accountIndex, change, addressIndex); + } + + base[2] = accountIndex + HARDENED_OFFSET; + base[3] = change; + base[4] = addressIndex; + return base; +}; + +const normalizeAddress = (address: Address, checksum = true): Address => { + if (!checksum) return address; + return checksumAddress(address); +}; + +export const evm: AssetModule = { + id: 'evm', + name: 'EVM', + coinType: 60, + curve: 'secp256k1', + defaultPath: DEFAULT_PATH, + supports: { + signTransaction: true, + signMessage: true, + signTypedData: true, + signArbitrary: false, + getPublicKey: true, + }, + create: (signer: Signer, options?: EvmAdapterOptions): EvmAdapter => { + const getAddress = async (params: EvmGetAddressParams = {}) => { + const path = resolvePath(params, options); + const checksum = params.checksum ?? options?.checksum ?? true; + const addr = await signer.getAddress(path); + return normalizeAddress(addr, checksum); + }; + + const getAddresses = async (params: GetAccountsParams = {}) => { + const startIndex = params.startIndex ?? 0; + const count = params.count ?? 1; + const addresses: Address[] = []; + for (let i = 0; i < count; i += 1) { + const path = resolvePath( + { accountIndex: options?.accountIndex ?? 0, change: params.change ?? options?.change ?? 0, addressIndex: startIndex + i }, + options, + ); + addresses.push(await signer.getAddress(path)); + } + return addresses; + }; + + const getPublicKey = async (params: EvmGetPublicKeyParams = {}) => { + const path = resolvePath(params, options); + return signer.getPublicKey(path, { compressed: params.compressed ?? true }); + }; + + const getAccount = async (params: EvmGetAddressParams = {}) => { + const address = await getAddress(params); + const path = resolvePath(params, options); + const publicKey = params.includePublicKey + ? await signer.getPublicKey(path, { compressed: true }) + : undefined; + return { address, publicKey, path, index: params.addressIndex }; + }; + + const sign = async (request: EvmSignRequest): Promise => { + // Ensure a path is always attached for signers that need it. + const path = (request as any).options?.path ?? resolvePath(undefined, options); + const next = { + ...request, + options: { ...(request as any).options, path }, + } as EvmSignRequest; + return signer.sign(next); + }; + + return { + getAddress, + getAddresses, + getPublicKey, + getAccount, + sign, + validateAddress: (address) => isAddress(address), + normalizeAddress: (address) => normalizeAddress(address, true), + signTransaction: (tx, signOptions) => + sign({ kind: 'transaction', payload: tx, options: signOptions }), + signMessage: (msg, signOptions) => + sign({ kind: 'message', payload: msg, options: signOptions }), + signTypedData: (typedData, signOptions) => + sign({ kind: 'typedData', payload: typedData, options: signOptions }), + }; + }, + utils: { + buildPath, + normalizeAddress, + }, +}; diff --git a/packages/assets/evm/src/index.ts b/packages/assets/evm/src/index.ts new file mode 100644 index 00000000..91d3d44b --- /dev/null +++ b/packages/assets/evm/src/index.ts @@ -0,0 +1,12 @@ +export { evm } from './asset'; + +export type { + Eip712Payload, + EvmAdapter, + EvmAdapterOptions, + EvmGetAddressParams, + EvmGetPublicKeyParams, + EvmRawTransaction, + EvmSignRequest, + Signer, +} from './asset'; diff --git a/packages/assets/evm/tsconfig.json b/packages/assets/evm/tsconfig.json new file mode 100644 index 00000000..d8636539 --- /dev/null +++ b/packages/assets/evm/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2022", "DOM"], + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./src", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "exclude": ["node_modules"], + "include": ["src"] +} diff --git a/packages/assets/evm/tsup.config.ts b/packages/assets/evm/tsup.config.ts new file mode 100644 index 00000000..12a41f8d --- /dev/null +++ b/packages/assets/evm/tsup.config.ts @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'tsup'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse( + readFileSync(resolve(__dirname, 'package.json'), 'utf-8'), +); + +const external = Object.keys({ + ...(pkg.dependencies ?? {}), + ...(pkg.peerDependencies ?? {}), +}); + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: './dist', + format: ['esm', 'cjs'], + target: 'node20', + sourcemap: true, + clean: true, + bundle: true, + dts: true, + silent: true, + outExtension: ({ format }) => ({ + js: format === 'esm' ? '.mjs' : '.cjs', + }), + external, + tsconfig: './tsconfig.json', +}); From d242819a98705859590a63cc0b30f9c7b13e9910 Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:07:59 +0300 Subject: [PATCH 5/7] feat(solana): add solana asset module --- packages/assets/solana/biome.json | 16 +++ packages/assets/solana/package.json | 45 ++++++ packages/assets/solana/src/asset.ts | 200 ++++++++++++++++++++++++++ packages/assets/solana/src/index.ts | 11 ++ packages/assets/solana/tsconfig.json | 25 ++++ packages/assets/solana/tsup.config.ts | 32 +++++ 6 files changed, 329 insertions(+) create mode 100644 packages/assets/solana/biome.json create mode 100644 packages/assets/solana/package.json create mode 100644 packages/assets/solana/src/asset.ts create mode 100644 packages/assets/solana/src/index.ts create mode 100644 packages/assets/solana/tsconfig.json create mode 100644 packages/assets/solana/tsup.config.ts diff --git a/packages/assets/solana/biome.json b/packages/assets/solana/biome.json new file mode 100644 index 00000000..71db724a --- /dev/null +++ b/packages/assets/solana/biome.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "linter": { + "enabled": true + }, + "organizeImports": { + "enabled": true + } +} + diff --git a/packages/assets/solana/package.json b/packages/assets/solana/package.json new file mode 100644 index 00000000..d8920ccd --- /dev/null +++ b/packages/assets/solana/package.json @@ -0,0 +1,45 @@ +{ + "name": "@gridplus/solana", + "version": "0.1.0", + "type": "module", + "description": "Solana asset interface for GridPlus SDK", + "scripts": { + "build": "tsup", + "lint": "biome check src", + "lint:fix": "biome check --write src", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/GridPlus/gridplus-sdk.git", + "directory": "packages/assets/solana" + }, + "dependencies": { + "@gridplus/asset-core": "workspace:*", + "@scure/base": "^1.2.6" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.0", + "@types/node": "^24.10.4", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + }, + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/packages/assets/solana/src/asset.ts b/packages/assets/solana/src/asset.ts new file mode 100644 index 00000000..2e78609b --- /dev/null +++ b/packages/assets/solana/src/asset.ts @@ -0,0 +1,200 @@ +import type { + Account, + Address, + AssetAdapter, + AssetModule, + DerivationPath, + GetAccountsParams, + GetAddressParams, + GetPublicKeyParams, + PublicKey, + SignResult, + Signer as CoreSigner, +} from '@gridplus/asset-core'; +import { base58 } from '@scure/base'; + +const HARDENED_OFFSET = 0x80000000; +const SOLANA_COIN_TYPE = 501; + +export type SolanaSignRequest = { + kind: 'transaction'; + /** Typically a Solana message bytes (compiled message), not a full transaction. */ + payload: Uint8Array | Buffer; + options?: { path?: DerivationPath }; +}; + +export type Signer = CoreSigner; + +export type SolanaGetAddressParams = Omit & { + includePublicKey?: boolean; +}; + +export type SolanaGetPublicKeyParams = Omit< + GetPublicKeyParams, + 'addressIndex' | 'compressed' +>; + +export type SolanaAdapterOptions = { + accountIndex?: number; + /** Solana derivations are typically fully hardened. This maps to the 4th path index. */ + change?: number; +}; + +export type SolanaAdapter = AssetAdapter< + SolanaSignRequest, + SolanaGetAddressParams, + GetAccountsParams, + SolanaGetPublicKeyParams, + Account +>; + +export const buildPath = (accountIndex: number, change: number): DerivationPath => { + return [ + 44 + HARDENED_OFFSET, + SOLANA_COIN_TYPE + HARDENED_OFFSET, + accountIndex + HARDENED_OFFSET, + change + HARDENED_OFFSET, + ]; +}; + +export const pubkeyToAddress = (pubkey: Uint8Array): Address => { + if (pubkey.length !== 32) { + throw new Error(`Invalid Solana pubkey length: ${pubkey.length}`); + } + return base58.encode(pubkey); +}; + +export const addressToPubkey = (address: Address): PublicKey => { + const bytes = base58.decode(address); + if (bytes.length !== 32) { + throw new Error(`Invalid Solana address length: ${bytes.length}`); + } + return bytes; +}; + +const validateAddress = (address: Address): boolean => { + try { + addressToPubkey(address); + return true; + } catch { + return false; + } +}; + +const normalizeAddress = (address: Address): Address => { + // Ensures canonical base58 encoding for the underlying 32-byte pubkey. + return pubkeyToAddress(addressToPubkey(address)); +}; + +const resolvePath = ( + params?: { path?: DerivationPath; accountIndex?: number; change?: number }, + options?: SolanaAdapterOptions, +): DerivationPath => { + if (params?.path) return params.path; + const accountIndex = params?.accountIndex ?? options?.accountIndex ?? 0; + const change = params?.change ?? options?.change ?? 0; + return buildPath(accountIndex, change); +}; + +export const solana: AssetModule< + SolanaSignRequest, + SolanaAdapter, + SolanaAdapterOptions +> = { + id: 'solana', + name: 'Solana', + coinType: SOLANA_COIN_TYPE, + curve: 'ed25519', + defaultPath: buildPath(0, 0), + supports: { + signTransaction: true, + signMessage: false, + signTypedData: false, + signArbitrary: false, + getPublicKey: true, + }, + create: (signer: Signer, options?: SolanaAdapterOptions): SolanaAdapter => { + const getPublicKey = async ( + params: SolanaGetPublicKeyParams = {}, + ): Promise => { + const path = resolvePath(params, options); + return signer.getPublicKey(path); + }; + + const getAddress = async (params: SolanaGetAddressParams = {}) => { + const pubkey = await getPublicKey(params); + return pubkeyToAddress(pubkey); + }; + + const getAddresses = async (params: GetAccountsParams = {}) => { + const startIndex = params.startIndex ?? 0; + const count = params.count ?? 1; + const addresses: Address[] = []; + for (let i = 0; i < count; i += 1) { + addresses.push( + await getAddress({ + accountIndex: startIndex + i, + change: params.change ?? options?.change ?? 0, + }), + ); + } + return addresses; + }; + + const getAccount = async (params: SolanaGetAddressParams = {}) => { + const address = await getAddress(params); + const path = resolvePath(params, options); + const publicKey = params.includePublicKey + ? await getPublicKey(params) + : undefined; + return { address, publicKey, path, index: params.accountIndex }; + }; + + const getAccounts = async (params: GetAccountsParams = {}) => { + const startIndex = params.startIndex ?? 0; + const count = params.count ?? 1; + const accounts: Account[] = []; + for (let i = 0; i < count; i += 1) { + const accountIndex = startIndex + i; + const path = resolvePath( + { accountIndex, change: params.change ?? options?.change ?? 0 }, + options, + ); + const publicKey = params.includePublicKey + ? await signer.getPublicKey(path) + : undefined; + const address = publicKey ? pubkeyToAddress(publicKey) : await getAddress({ path }); + accounts.push({ address, publicKey, path, index: accountIndex }); + } + return accounts; + }; + + const sign = async (request: SolanaSignRequest): Promise => { + const path = request.options?.path ?? resolvePath(undefined, options); + const next: SolanaSignRequest = { + ...request, + options: { ...(request.options ?? {}), path }, + }; + return signer.sign(next); + }; + + return { + getAddress, + getAddresses, + getPublicKey, + getAccount, + getAccounts, + sign, + validateAddress, + normalizeAddress, + }; + }, + utils: { + buildPath, + pubkeyToAddress, + addressToPubkey, + validateAddress, + normalizeAddress, + }, +}; + diff --git a/packages/assets/solana/src/index.ts b/packages/assets/solana/src/index.ts new file mode 100644 index 00000000..f176dfb6 --- /dev/null +++ b/packages/assets/solana/src/index.ts @@ -0,0 +1,11 @@ +export { solana } from './asset'; +export { addressToPubkey, buildPath, pubkeyToAddress } from './asset'; + +export type { + Signer, + SolanaAdapter, + SolanaAdapterOptions, + SolanaGetAddressParams, + SolanaGetPublicKeyParams, + SolanaSignRequest, +} from './asset'; diff --git a/packages/assets/solana/tsconfig.json b/packages/assets/solana/tsconfig.json new file mode 100644 index 00000000..a9c803ac --- /dev/null +++ b/packages/assets/solana/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2022", "DOM"], + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./src", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "types": ["node"] + }, + "exclude": ["node_modules"], + "include": ["src"] +} + diff --git a/packages/assets/solana/tsup.config.ts b/packages/assets/solana/tsup.config.ts new file mode 100644 index 00000000..10caaf24 --- /dev/null +++ b/packages/assets/solana/tsup.config.ts @@ -0,0 +1,32 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'tsup'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse( + readFileSync(resolve(__dirname, 'package.json'), 'utf-8'), +); + +const external = Object.keys({ + ...(pkg.dependencies ?? {}), + ...(pkg.peerDependencies ?? {}), +}); + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: './dist', + format: ['esm', 'cjs'], + target: 'node20', + sourcemap: true, + clean: true, + bundle: true, + dts: true, + silent: true, + outExtension: ({ format }) => ({ + js: format === 'esm' ? '.mjs' : '.cjs', + }), + external, + tsconfig: './tsconfig.json', +}); + From a7be7d6eeef1b16d4400d1628e64cebaa0b3191a Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:08:08 +0300 Subject: [PATCH 6/7] feat(sdk): add lattice signers bridge for asset modules --- packages/sdk/package.json | 4 + packages/sdk/src/assets/index.ts | 8 + packages/sdk/src/assets/lattice.ts | 540 +++++++++++++++++++++++++++++ packages/sdk/src/index.ts | 1 + 4 files changed, 553 insertions(+) create mode 100644 packages/sdk/src/assets/index.ts create mode 100644 packages/sdk/src/assets/lattice.ts diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 45b12cbc..0c445391 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -55,7 +55,11 @@ "url": "https://github.com/GridPlus/gridplus-sdk.git" }, "dependencies": { + "@gridplus/asset-core": "workspace:*", "@gridplus/btc": "workspace:*", + "@gridplus/cosmos": "workspace:*", + "@gridplus/evm": "workspace:*", + "@gridplus/solana": "workspace:*", "@gridplus/types": "workspace:*", "@ethereumjs/common": "^10.0.0", "@ethereumjs/rlp": "^10.0.0", diff --git a/packages/sdk/src/assets/index.ts b/packages/sdk/src/assets/index.ts new file mode 100644 index 00000000..93b209df --- /dev/null +++ b/packages/sdk/src/assets/index.ts @@ -0,0 +1,8 @@ +export { + createLatticeBtcSigner, + createLatticeCosmosSigner, + createLatticeEvmSigner, + createLatticeSolanaSigner, +} from './lattice'; + +export type { LatticeSignerOptions } from './lattice'; diff --git a/packages/sdk/src/assets/lattice.ts b/packages/sdk/src/assets/lattice.ts new file mode 100644 index 00000000..3334eca2 --- /dev/null +++ b/packages/sdk/src/assets/lattice.ts @@ -0,0 +1,540 @@ +import type { + Address, + DerivationPath, + PublicKey, + SignResult, +} from '@gridplus/asset-core'; +import { + BTC_COIN_TYPES, + BTC_PURPOSES, + HARDENED_OFFSET, + format, + type BtcCoinType, + type BtcPurpose, + type BtcSignRequest, + type Signer as BtcSigner, + type XpubOptions, + type XpubsOptions, +} from '@gridplus/btc'; +import type { Signer as CosmosSigner, CosmosSignRequest } from '@gridplus/cosmos'; +import type { + EvmRawTransaction, + EvmSignRequest, + Signer as EvmSigner, +} from '@gridplus/evm'; +import { pubkeyToAddress, type Signer as SolanaSigner, type SolanaSignRequest } from '@gridplus/solana'; +import { Hash } from 'ox'; +import { + type Hex, + type TransactionSerializable, + type TransactionSerializableEIP7702, + serializeTransaction, +} from 'viem'; +import { CURRENCIES } from '@gridplus/types'; +import { EXTERNAL } from '../constants'; +import { queue } from '../api/utilities'; +import { fetchDecoder } from '../functions/fetchDecoder'; + +function isHexString(value: unknown): value is Hex { + return typeof value === 'string' && value.startsWith('0x'); +} + +function toBuffer(value: unknown): Buffer { + if (Buffer.isBuffer(value)) return value; + if (value instanceof Uint8Array) return Buffer.from(value); + if (typeof value === 'string') { + const hex = value.startsWith('0x') ? value.slice(2) : value; + return Buffer.from(hex, 'hex'); + } + throw new Error('Unsupported byte input'); +} + +function compressSecp256k1Pubkey(pubkey: Uint8Array): Uint8Array { + if (pubkey.length === 33 && (pubkey[0] === 0x02 || pubkey[0] === 0x03)) { + return pubkey; + } + if (pubkey.length === 65 && pubkey[0] === 0x04) { + const x = pubkey.slice(1, 33); + const yLastByte = pubkey[64]; + const prefix = yLastByte % 2 === 0 ? 0x02 : 0x03; + const out = new Uint8Array(33); + out[0] = prefix; + out.set(x, 1); + return out; + } + return pubkey; +} + +function parseHexBytes(value: unknown, expectedLen?: number): Uint8Array { + if (typeof value === 'string') { + const hex = value.startsWith('0x') ? value.slice(2) : value; + const buf = Buffer.from(hex, 'hex'); + if (expectedLen !== undefined && buf.length !== expectedLen) { + // Keep padding logic conservative: only left-pad when the value is shorter. + if (buf.length < expectedLen) { + const out = Buffer.alloc(expectedLen); + buf.copy(out, expectedLen - buf.length); + return new Uint8Array(out); + } + } + return new Uint8Array(buf); + } + if (Buffer.isBuffer(value)) return new Uint8Array(value); + if (value instanceof Uint8Array) return value; + throw new Error('Unsupported signature component type'); +} + +function normalizeBtcSignedTxHex(tx?: string): string | undefined { + if (!tx) return undefined; + return tx.startsWith('0x') ? tx : `0x${tx}`; +} + +function normalizeTxHashHex(txHash?: string): string | undefined { + if (!txHash) return undefined; + return txHash.startsWith('0x') ? txHash : `0x${txHash}`; +} + +function buildSigResultFromRsv(sig: { + r?: unknown; + s?: unknown; + v?: unknown; +}): { signature: { bytes: Uint8Array; r?: Uint8Array; s?: Uint8Array; v?: bigint | number } } { + const r = sig.r !== undefined ? parseHexBytes(sig.r, 32) : undefined; + const s = sig.s !== undefined ? parseHexBytes(sig.s, 32) : undefined; + + let v: bigint | number | undefined; + if (typeof sig.v === 'bigint') v = sig.v; + else if (typeof sig.v === 'number') v = sig.v; + else if (typeof sig.v === 'string') v = BigInt(sig.v); + else if (Buffer.isBuffer(sig.v) || sig.v instanceof Uint8Array) { + const buf = Buffer.from(sig.v as any); + v = buf.length === 0 ? 0n : BigInt(`0x${buf.toString('hex')}`); + } + + const bytes = + r && s ? new Uint8Array(Buffer.concat([Buffer.from(r), Buffer.from(s)])) : new Uint8Array(); + return { signature: { bytes, r, s, v } }; +} + +export type LatticeSignerOptions = { + /** If true, try to fetch a calldata decoder for EVM tx requests when possible. */ + fetchEvmDecoder?: boolean; +}; + +export function createLatticeBtcSigner(): BtcSigner { + const getAddress = async (path: DerivationPath): Promise
=> { + const res = (await queue((client) => + client.getAddresses({ startPath: path, n: 1 }), + )) as any[]; + const addr = res?.[0]; + if (typeof addr !== 'string') { + throw new Error('Device did not return a BTC address string'); + } + return addr; + }; + + const getPublicKey = async ( + path: DerivationPath, + options?: unknown, + ): Promise => { + const res = (await queue((client) => + client.getAddresses({ + startPath: path, + n: 1, + flag: EXTERNAL.GET_ADDR_FLAGS.SECP256K1_PUB, + }), + )) as any[]; + const pub = res?.[0]; + if (!pub) throw new Error('Device did not return a public key'); + const pubBytes = Buffer.from(pub); + const wantCompressed = + typeof (options as any)?.compressed === 'boolean' + ? Boolean((options as any).compressed) + : false; + return wantCompressed + ? compressSecp256k1Pubkey(new Uint8Array(pubBytes)) + : new Uint8Array(pubBytes); + }; + + const sign = async (request: BtcSignRequest): Promise => { + if (request.kind !== 'transaction') { + throw new Error(`Unsupported BTC sign request kind: ${request.kind}`); + } + const res = await queue((client) => + client.sign({ data: request.payload as any, currency: CURRENCIES.BTC }), + ); + return { + signature: { bytes: new Uint8Array() }, + signedPayload: normalizeBtcSignedTxHex((res as any).tx), + txHash: normalizeTxHashHex((res as any).txHash), + metadata: { + changeRecipient: (res as any).changeRecipient, + sigs: (res as any).sigs, + }, + }; + }; + + const getXpub = async (options: XpubOptions): Promise => { + const { purpose, coinType = BTC_COIN_TYPES.MAINNET, account = 0 } = options; + const startPath: DerivationPath = [ + purpose + HARDENED_OFFSET, + coinType + HARDENED_OFFSET, + account + HARDENED_OFFSET, + ]; + const network = + coinType === BTC_COIN_TYPES.TESTNET ? 'testnet' : 'mainnet'; + const res = (await queue((client) => + client.getAddresses({ + startPath, + n: 1, + flag: EXTERNAL.GET_ADDR_FLAGS.SECP256K1_XPUB, + }), + )) as any[]; + const xpub = res?.[0]; + if (typeof xpub !== 'string') { + throw new Error('Device did not return an xpub string'); + } + return format(xpub, purpose, network); + }; + + const getXpubs = async ( + options: XpubsOptions, + ): Promise> => { + const { purposes, coinType = BTC_COIN_TYPES.MAINNET, account = 0 } = options; + const results = new Map(); + for (const purpose of purposes) { + results.set( + purpose, + await getXpub({ purpose, coinType, account }), + ); + } + return results; + }; + + const getAllXpubs = async ( + coinType: BtcCoinType = BTC_COIN_TYPES.MAINNET, + account = 0, + ): Promise<{ xpub: string; ypub: string; zpub: string }> => { + const xpubs = await getXpubs({ + purposes: [BTC_PURPOSES.LEGACY, BTC_PURPOSES.WRAPPED, BTC_PURPOSES.NATIVE], + coinType, + account, + }); + const xpub = xpubs.get(BTC_PURPOSES.LEGACY); + const ypub = xpubs.get(BTC_PURPOSES.WRAPPED); + const zpub = xpubs.get(BTC_PURPOSES.NATIVE); + if (!xpub || !ypub || !zpub) { + throw new Error('Failed to fetch all xpubs'); + } + return { xpub, ypub, zpub }; + }; + + return { + getAddress, + getPublicKey, + sign, + getXpub, + getXpubs, + getAllXpubs, + }; +} + +function isRawEvmTx(value: TransactionSerializable | EvmRawTransaction): value is EvmRawTransaction { + return ( + typeof value === 'string' || + value instanceof Uint8Array || + Buffer.isBuffer(value) + ); +} + +function normalizeRawEvmTx(tx: EvmRawTransaction): Hex | Buffer { + if (typeof tx === 'string') { + return (tx.startsWith('0x') ? tx : (`0x${tx}` as Hex)) as Hex; + } + return Buffer.from(tx); +} + +function getEvmEncodingType(tx: TransactionSerializable): number { + if ((tx as any).type === 'eip7702') { + const eip7702 = tx as TransactionSerializableEIP7702; + const hasAuthList = + eip7702.authorizationList && eip7702.authorizationList.length > 0; + return hasAuthList + ? EXTERNAL.SIGNING.ENCODINGS.EIP7702_AUTH_LIST + : EXTERNAL.SIGNING.ENCODINGS.EIP7702_AUTH; + } + return EXTERNAL.SIGNING.ENCODINGS.EVM; +} + +export function createLatticeEvmSigner( + options: LatticeSignerOptions = {}, +): EvmSigner { + return { + getAddress: async (path: DerivationPath): Promise
=> { + const res = (await queue((client) => + client.getAddresses({ startPath: path, n: 1 }), + )) as any[]; + const addr = res?.[0]; + if (typeof addr !== 'string') { + throw new Error('Device did not return an EVM address string'); + } + return addr; + }, + getPublicKey: async (path: DerivationPath, opts?: unknown): Promise => { + const res = (await queue((client) => + client.getAddresses({ + startPath: path, + n: 1, + flag: EXTERNAL.GET_ADDR_FLAGS.SECP256K1_PUB, + }), + )) as any[]; + const pub = res?.[0]; + if (!pub) throw new Error('Device did not return a public key'); + const pubBytes = Buffer.from(pub); + const wantCompressed = + typeof (opts as any)?.compressed === 'boolean' + ? Boolean((opts as any).compressed) + : false; + return wantCompressed + ? compressSecp256k1Pubkey(new Uint8Array(pubBytes)) + : new Uint8Array(pubBytes); + }, + sign: async (request: EvmSignRequest): Promise => { + const path = (request as any).options?.path as DerivationPath | undefined; + if (!path || path.length < 2) { + throw new Error('EVM sign request missing signer path'); + } + + if (request.kind === 'transaction') { + const isRaw = isRawEvmTx(request.payload); + const payload = isRaw + ? normalizeRawEvmTx(request.payload as EvmRawTransaction) + : serializeTransaction(request.payload as TransactionSerializable); + + const encodingType = isRaw + ? EXTERNAL.SIGNING.ENCODINGS.EVM + : getEvmEncodingType(request.payload as TransactionSerializable); + + let decoder: Buffer | undefined; + if (!isRaw && options.fetchEvmDecoder) { + const tx = request.payload as TransactionSerializable; + if ('data' in tx && 'to' in tx && 'chainId' in tx) { + decoder = await fetchDecoder({ + data: (tx as any).data, + to: (tx as any).to, + chainId: (tx as any).chainId, + } as any); + } + } + + const signPayload = { + signerPath: path, + curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, + hashType: EXTERNAL.SIGNING.HASHES.KECCAK256, + encodingType, + payload, + decoder, + }; + + const res = await queue((client) => client.sign({ data: signPayload })); + + const sig = (res as any).sig ?? {}; + const { signature } = buildSigResultFromRsv(sig); + const pubkey = (res as any).pubkey + ? compressSecp256k1Pubkey(new Uint8Array(Buffer.from((res as any).pubkey))) + : undefined; + + const signedPayload = + (res as any).viemTx ?? (res as any).tx ?? undefined; + const txHash = + typeof signedPayload === 'string' && isHexString(signedPayload) + ? (`0x${Buffer.from(Hash.keccak256(toBuffer(signedPayload))).toString('hex')}` as string) + : undefined; + + return { + signature, + publicKey: pubkey, + signedPayload, + txHash, + metadata: { + viemTx: (res as any).viemTx, + }, + }; + } + + if (request.kind === 'message') { + const protocol = (request as any).options?.protocol ?? 'signPersonal'; + const res = await queue((client) => + client.sign({ + data: { + signerPath: path, + curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, + hashType: EXTERNAL.SIGNING.HASHES.KECCAK256, + payload: request.payload as any, + protocol, + }, + currency: CURRENCIES.ETH_MSG, + }), + ); + const sig = (res as any).sig ?? {}; + const { signature } = buildSigResultFromRsv(sig); + return { + signature, + signedPayload: undefined, + metadata: { + signer: (res as any).signer, + }, + }; + } + + if (request.kind === 'typedData') { + const res = await queue((client) => + client.sign({ + data: { + signerPath: path, + curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, + hashType: EXTERNAL.SIGNING.HASHES.KECCAK256, + payload: request.payload as any, + protocol: 'eip712', + }, + currency: CURRENCIES.ETH_MSG, + }), + ); + const sig = (res as any).sig ?? {}; + const { signature } = buildSigResultFromRsv(sig); + return { + signature, + signedPayload: undefined, + metadata: { + signer: (res as any).signer, + }, + }; + } + + throw new Error(`Unsupported EVM sign request kind: ${(request as any).kind}`); + }, + }; +} + +export function createLatticeSolanaSigner(): SolanaSigner { + const getPublicKey = async (path: DerivationPath): Promise => { + const res = (await queue((client) => + client.getAddresses({ + startPath: path, + n: 1, + flag: EXTERNAL.GET_ADDR_FLAGS.ED25519_PUB, + }), + )) as any[]; + const pub = res?.[0]; + if (!pub) throw new Error('Device did not return a public key'); + const pubBytes = Buffer.from(pub); + return new Uint8Array(pubBytes.slice(0, 32)); + }; + + const getAddress = async (path: DerivationPath): Promise
=> { + const pubkey = await getPublicKey(path); + return pubkeyToAddress(pubkey); + }; + + const sign = async (request: SolanaSignRequest): Promise => { + if (request.kind !== 'transaction') { + throw new Error(`Unsupported Solana sign request kind: ${request.kind}`); + } + const path = (request as any).options?.path as DerivationPath | undefined; + if (!path || path.length < 2) { + throw new Error('Solana sign request missing signer path'); + } + + const signPayload = { + signerPath: path, + curveType: EXTERNAL.SIGNING.CURVES.ED25519, + hashType: EXTERNAL.SIGNING.HASHES.NONE, + encodingType: EXTERNAL.SIGNING.ENCODINGS.SOLANA, + payload: toBuffer(request.payload as any), + }; + + const res = await queue((client) => client.sign({ data: signPayload })); + const sig = (res as any).sig ?? {}; + const { signature } = buildSigResultFromRsv(sig); + + const pubkey = (res as any).pubkey + ? new Uint8Array(Buffer.from((res as any).pubkey).slice(0, 32)) + : undefined; + + return { + signature, + publicKey: pubkey, + }; + }; + + return { + getAddress, + getPublicKey, + sign, + }; +} + +export function createLatticeCosmosSigner(): CosmosSigner { + return { + getAddress: async (path: DerivationPath): Promise
=> { + void path; + // Lattice does not support Cosmos bech32 addresses via getAddresses. + // Use @gridplus/cosmos adapter `getAddress()` which derives bech32 from the pubkey + HRP. + throw new Error( + 'Cosmos addresses must be derived from pubkey (use @gridplus/cosmos adapter)', + ); + }, + getPublicKey: async (path: DerivationPath, opts?: unknown): Promise => { + const res = (await queue((client) => + client.getAddresses({ + startPath: path, + n: 1, + flag: EXTERNAL.GET_ADDR_FLAGS.SECP256K1_PUB, + }), + )) as any[]; + const pub = res?.[0]; + if (!pub) throw new Error('Device did not return a public key'); + const pubBytes = Buffer.from(pub); + const wantCompressed = + typeof (opts as any)?.compressed === 'boolean' + ? Boolean((opts as any).compressed) + : true; + return wantCompressed + ? compressSecp256k1Pubkey(new Uint8Array(pubBytes)) + : new Uint8Array(pubBytes); + }, + sign: async (request: CosmosSignRequest): Promise => { + if (request.kind !== 'transaction') { + throw new Error(`Unsupported Cosmos sign request kind: ${request.kind}`); + } + const path = (request as any).options?.path as DerivationPath | undefined; + if (!path || path.length < 2) { + throw new Error('Cosmos sign request missing signer path'); + } + + const signPayload = { + signerPath: path, + curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, + hashType: EXTERNAL.SIGNING.HASHES.SHA256, + encodingType: EXTERNAL.SIGNING.ENCODINGS.COSMOS, + payload: Buffer.from(request.payload as any), + }; + + const res = await queue((client) => client.sign({ data: signPayload })); + const sig = (res as any).sig ?? {}; + const { signature } = buildSigResultFromRsv(sig); + + const pubkey = (res as any).pubkey + ? compressSecp256k1Pubkey(new Uint8Array(Buffer.from((res as any).pubkey))) + : undefined; + + return { + signature, + publicKey: pubkey, + metadata: { + mode: (request as any).options?.mode, + }, + }; + }, + }; +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 53feb6e8..b9bc7074 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -3,4 +3,5 @@ export { Client } from './client'; export { EXTERNAL as Constants } from './constants'; export { EXTERNAL as Utils } from './util'; export * from './api'; +export * from './assets'; export * as btc from './btc'; From b1e88107cf85bc17f10acc988638e2a716e391a0 Mon Sep 17 00:00:00 2001 From: baha Date: Tue, 10 Feb 2026 17:08:15 +0300 Subject: [PATCH 7/7] chore(monorepo): include asset packages in scripts and lockfile --- package.json | 8 ++-- pnpm-lock.yaml | 105 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index bebb16cd..f4b01950 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,12 @@ "private": true, "packageManager": "pnpm@10.6.2", "scripts": { - "build": "turbo run build --filter=gridplus-sdk --filter=@gridplus/btc --filter=@gridplus/types", + "build": "turbo run build --filter=gridplus-sdk --filter=@gridplus/asset-core --filter=@gridplus/btc --filter=@gridplus/cosmos --filter=@gridplus/evm --filter=@gridplus/solana --filter=@gridplus/types", "test": "turbo run test --filter=gridplus-sdk --filter=@gridplus/btc", "test-unit": "turbo run test-unit --filter=gridplus-sdk --filter=@gridplus/btc", - "lint": "turbo run lint --filter=gridplus-sdk --filter=@gridplus/btc --filter=@gridplus/types", - "lint:fix": "turbo run lint:fix --filter=gridplus-sdk --filter=@gridplus/btc --filter=@gridplus/types", - "typecheck": "turbo run typecheck --filter=gridplus-sdk --filter=@gridplus/btc --filter=@gridplus/types", + "lint": "turbo run lint --filter=gridplus-sdk --filter=@gridplus/asset-core --filter=@gridplus/btc --filter=@gridplus/cosmos --filter=@gridplus/evm --filter=@gridplus/solana --filter=@gridplus/types", + "lint:fix": "turbo run lint:fix --filter=gridplus-sdk --filter=@gridplus/asset-core --filter=@gridplus/btc --filter=@gridplus/cosmos --filter=@gridplus/evm --filter=@gridplus/solana --filter=@gridplus/types", + "typecheck": "turbo run typecheck --filter=gridplus-sdk --filter=@gridplus/asset-core --filter=@gridplus/btc --filter=@gridplus/cosmos --filter=@gridplus/evm --filter=@gridplus/solana --filter=@gridplus/types", "e2e": "turbo run e2e --filter=gridplus-sdk", "docs:build": "pnpm --filter gridplus-sdk-docs run build", "docs:start": "pnpm --filter gridplus-sdk-docs run start" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c63b2149..eb6d6c66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,26 @@ importers: specifier: ^2.3.0 version: 2.7.4 - packages/btc: + packages/assets/asset-core: + devDependencies: + '@biomejs/biome': + specifier: ^1.9.0 + version: 1.9.4 + '@types/node': + specifier: ^24.10.4 + version: 24.10.4 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@24.10.4))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + + packages/assets/btc: dependencies: + '@gridplus/asset-core': + specifier: workspace:* + version: link:../asset-core bs58check: specifier: ^4.0.0 version: 4.0.0 @@ -37,6 +55,75 @@ importers: specifier: 3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(jiti@1.21.7)(msw@2.12.7(@types/node@24.10.4)(typescript@5.9.3))(terser@5.44.1)(tsx@4.21.0) + packages/assets/cosmos: + dependencies: + '@gridplus/asset-core': + specifier: workspace:* + version: link:../asset-core + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 + bech32: + specifier: ^2.0.0 + version: 2.0.0 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.0 + version: 1.9.4 + '@types/node': + specifier: ^24.10.4 + version: 24.10.4 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@24.10.4))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + + packages/assets/evm: + dependencies: + '@gridplus/asset-core': + specifier: workspace:* + version: link:../asset-core + viem: + specifier: ^2.37.8 + version: 2.43.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.5) + devDependencies: + '@biomejs/biome': + specifier: ^1.9.0 + version: 1.9.4 + '@types/node': + specifier: ^24.10.4 + version: 24.10.4 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@24.10.4))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + + packages/assets/solana: + dependencies: + '@gridplus/asset-core': + specifier: workspace:* + version: link:../asset-core + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.0 + version: 1.9.4 + '@types/node': + specifier: ^24.10.4 + version: 24.10.4 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@24.10.4))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + packages/docs: dependencies: '@docusaurus/core': @@ -140,9 +227,21 @@ importers: '@ethereumjs/tx': specifier: ^10.0.0 version: 10.1.0 + '@gridplus/asset-core': + specifier: workspace:* + version: link:../assets/asset-core '@gridplus/btc': specifier: workspace:* - version: link:../btc + version: link:../assets/btc + '@gridplus/cosmos': + specifier: workspace:* + version: link:../assets/cosmos + '@gridplus/evm': + specifier: workspace:* + version: link:../assets/evm + '@gridplus/solana': + specifier: workspace:* + version: link:../assets/solana '@gridplus/types': specifier: workspace:* version: link:../types @@ -10477,7 +10576,7 @@ snapshots: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.2.1 '@noble/hashes': 1.8.0 - '@scure/base': 1.1.9 + '@scure/base': 1.2.6 '@types/debug': 4.1.12 '@types/lodash': 4.17.21 debug: 4.4.3