Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stacked] add ledger ton support #24

Merged
merged 2 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
395 changes: 383 additions & 12 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions packages/signer-ledger-ton/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2024 Chorus One AG

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
32 changes: 32 additions & 0 deletions packages/signer-ledger-ton/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Chorus One SDK: Ledger TON Signer

Ledger signer for the Chorus One SDK, used for signing TON transactions

## Installation

In the project’s root directory, run the following command:

```bash
npm install @chorus-one/signer-ledger-ton --save
```

## Setting Up the LedgerTonSigner

With your mnemonic and HD path ready, you can now configure and initialize the LedgerTonSigner:

```javascript
import { LedgerTonSigner } from '@chorus-one/signer-ledger-ton'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'

const signer = new LedgerTonSigner({
transport: await TransportNodeHid.create(),
accounts: [{ hdPath: 'your-hd-path' }],
bounceable: true
})

await signer.init()
```

## License

The Chorus One SDK is licensed under the Apache 2.0 License. For more detailed information, please refer to the [LICENSE](./LICENSE) file in the repository.
41 changes: 41 additions & 0 deletions packages/signer-ledger-ton/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@chorus-one/signer-ledger-ton",
"version": "1.0.0",
"description": "Ledger signer for the Chorus One SDK, used for signing TON transactions",
"scripts": {
"build": "rm -fr dist/* && tsc -p tsconfig.mjs.json --outDir dist/mjs && tsc -p tsconfig.cjs.json --outDir dist/cjs && bash ../../scripts/fix-package-json"
},
"main": "dist/cjs/index.js",
"module": "dist/mjs/index.js",
"types": "dist/mjs/index.d.ts",
"exports": {
".": {
"import": "./dist/mjs/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/mjs/index.d.ts"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/ChorusOne/chorus-one-sdk.git",
"directory": "packages/signer-ledger-ton"
},
"homepage": "https://chorus-one.gitbook.io/sdk",
"keywords": [
"chorus-one",
"sdk",
"signer",
"ledger",
"ton"
],
"author": "Chrous One AG",
"license": "Apache-2.0",
"dependencies": {
"@chorus-one/signer": "^1.0.0",
"@chorus-one/ton": "^2.0.2",
"@chorus-one/utils": "^1.0.0",
"@ledgerhq/hw-transport": "^6.31.0",
"@ton-community/ton-ledger": "^7.2.0",
"@ton/ton": "^15.1.0"
}
}
2 changes: 2 additions & 0 deletions packages/signer-ledger-ton/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { LedgerTonSigner } from './signer'
export { LedgerTonSignerConfig } from './types.d'
186 changes: 186 additions & 0 deletions packages/signer-ledger-ton/src/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { nopLogger } from '@chorus-one/utils'
import type { Logger } from '@chorus-one/utils'
import type { LedgerTonSignerConfig } from './types'
import type { Signature, SignerData } from '@chorus-one/signer'
import { TonTransport as Ton, TonPayloadFormat } from '@ton-community/ton-ledger'
import Transport from '@ledgerhq/hw-transport'
import { TonSigningData } from '@chorus-one/ton'
import { Address, Cell } from '@ton/ton'

/**
* The LedgerTonSigner in the Chorus One SDK is a specialized implementation of the Signer interface that
* utilizes a Ledger Ton App
*
* Ledger is known for its advanced security features, including secure hardware wallets and robust key management,
* making it an ideal choice for retail and enterprise customers
*/
export class LedgerTonSigner {
private readonly config: LedgerTonSignerConfig
private readonly transport: Transport
private accounts: Map<string, { hdPath: string; publicKey: Uint8Array }>
private app?: Ton
private logger: Logger

/**
* Constructs a new LedgerTonSigner.
*
* @param params - The parameters required to initialize the LedgerTonSigner
* @param params.transport - The Ledger HW transport object to use for communication with the Ledger device
* @param params.accounts - An array of account objects, each containing an HD path
* @param params.bounceable - Address derivation setting to enable bounceable addresses
* @param params.logger - (Optional) A logger to use for logging messages, i.e `console`
*
* @returns A new instance of LedgerTonSigner.
*/
constructor (params: { transport: Transport; accounts: [{ hdPath: string }]; bounceable: boolean; logger?: Logger }) {
const { transport, ...config } = params

this.transport = transport
this.config = config
this.logger = params.logger ?? nopLogger
this.accounts = new Map()
}

/**
* Initializes the signer, performing any necessary setup or configuration.
* @returns A promise that resolves once the initialization is complete.
*/
async init (): Promise<void> {
const app = new Ton(this.transport)
this.app = app

this.config.accounts.forEach(async (account: { hdPath: string }) => {
const hdPath = account.hdPath
.replace('m/', '')
.split('/')
.map((i: string) => Number(i.replace("'", '')))

// TON Ledger app requires 6 elements in the HD path
if (hdPath.length < 6) {
throw new Error('Invalid path, expected at least 5 elements')
}

const network = hdPath[2]
const chain = hdPath[3]
const testOnly = network === 1

const response = await app.getAddress(hdPath, {
bounceable: this.config.bounceable,
chain,
testOnly
})

this.accounts.set(response.address.toLowerCase(), {
hdPath: account.hdPath,
publicKey: response.publicKey
})
})
}

/**
* Signs the provided data using the private key associated with the signer's address.
*
* @param signerAddress - The address of the signer
* @param signerData - The data to be signed, which can be a raw message or custom data
*
* @returns A promise that resolves to an object containing the signature and public key.
*/
async sign (signerAddress: string, signerData: SignerData): Promise<{ sig: Signature; pk: Uint8Array }> {
if (this.app === undefined) {
throw new Error('LedgerTonSigner not initialized. Did you forget to call init()?')
}
this.logger.info(`signing data from address: ${signerAddress} with Ledger Ton App`)

if (signerData.data === undefined) {
throw new Error('missing message to sign')
}

const data: TonSigningData = signerData.data
const account = this.getAccount(signerAddress)
const path = account.hdPath
.replace('m/', '')
.split('/')
.map((i: string) => Number(i.replace("'", '')))

if (data.tx.messages !== undefined && data.tx.messages.length > 1) {
throw new Error(
'Ledger App can only sign one message per transaction. The requested TX has ' +
data.tx.messages.length +
' messages'
)
}

const msg = data.tx.messages !== undefined ? data.tx.messages[0] : undefined
const timeout = data.txArgs.timeout || data.tx.validUntil || Math.floor(Date.now() / 1000) + 180 // 3 minutes

let payload: TonPayloadFormat | undefined = undefined
if (msg !== undefined) {
if (msg.payload !== undefined && !(msg.payload instanceof Cell)) {
throw new Error('Ledger App can only sign Cell payloads')
}
payload = {
type: 'unsafe',
message: msg.payload === undefined ? Cell.EMPTY : (msg.payload as Cell)
}
}

const common = {
sendMode: data.txArgs.sendMode,
seqno: data.txArgs.seqno,
timeout,
payload
}

const args =
msg !== undefined
? {
to: Address.parse(msg.address),
bounce: msg.bounceable,
amount: msg.amount,
stateInit: msg.stateInit,
...common
}
: {
// A transaction without message is still a valid TX (e.g. for deploying wallet contract) however,
// the Ledger App requires one message to be present in the transaction,
// therefore we create a dummy message with zero amount and no stateInit
// that sends 0 tokens to self
to: Address.parse(signerAddress),
bounce: false,
amount: BigInt(0),
stateInit: undefined,
...common
}

const signedTx: Cell = await this.app.signTransaction(path, args)
const sig = {
// sneaking signed transaction into sig field
fullSig: signedTx.toBoc().toString('hex'),
r: '',
s: '',
v: 0
}

return { sig, pk: account.publicKey }
}

/**
* Retrieves the public key associated with the signer's address.
*
* @param address - The address of the signer
*
* @returns A promise that resolves to a Uint8Array representing the public key.
*/
async getPublicKey (address: string): Promise<Uint8Array> {
return this.getAccount(address).publicKey
}

private getAccount (address: string): { hdPath: string; publicKey: Uint8Array } {
const account = this.accounts.get(address.toLowerCase())
if (account === undefined) {
throw new Error(`no account found for address: ${address}`)
}

return account
}
}
9 changes: 9 additions & 0 deletions packages/signer-ledger-ton/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @ignore */
export interface LedgerTonSignerConfig {
// Whether derived addresses should be bounceable
bounceable: boolean

// BIP44 address derivation path
// e.g. 44'/607'/0'/0'/0'/0'
accounts: [{ hdPath: string }]
}
9 changes: 9 additions & 0 deletions packages/signer-ledger-ton/tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.cjs.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../signer" }, { "path": "../utils" }, { "path": "../ton" }]
}
14 changes: 14 additions & 0 deletions packages/signer-ledger-ton/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"baseUrl": ".",
"paths": {
"@chorus-one/signer": ["../signer/src"],
"@chorus-one/utils": ["../utils/src"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../signer" }, { "path": "../utils" }, { "path": "../ton" }]
}
9 changes: 9 additions & 0 deletions packages/signer-ledger-ton/tsconfig.mjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.mjs.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../signer" }, { "path": "../utils" }, { "path": "../ton" }]
}
2 changes: 2 additions & 0 deletions packages/staking-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
"@chorus-one/near": "^1.0.0",
"@chorus-one/signer": "^1.0.0",
"@chorus-one/signer-fireblocks": "^1.0.0",
"@chorus-one/signer-ledger-ton": "^1.0.0",
"@chorus-one/signer-local": "^1.0.0",
"@chorus-one/solana": "^1.0.0",
"@chorus-one/substrate": "^1.0.1",
"@chorus-one/ton": "^2.0.0",
"@chorus-one/utils": "^1.0.0",
"@commander-js/extra-typings": "^11.1.0",
"@ledgerhq/hw-transport-node-hid": "^6.29.5",
"chalk": "^4.1.2"
}
}
3 changes: 2 additions & 1 deletion packages/staking-cli/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// value (not only the type)
export enum SignerType {
FIREBLOCKS = 'fireblocks',
LOCAL = 'local'
LOCAL = 'local',
LEDGER = 'ledger'
}

export enum NetworkType {
Expand Down
15 changes: 15 additions & 0 deletions packages/staking-cli/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { SignerType } from './enums'
import { FireblocksSigner } from '@chorus-one/signer-fireblocks'
import { LocalSigner } from '@chorus-one/signer-local'
import { Logger } from '@chorus-one/utils'
import { LedgerTonSigner } from '@chorus-one/signer-ledger-ton'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'

export async function newSigner (
config: Config,
Expand Down Expand Up @@ -50,5 +52,18 @@ export async function newSigner (
...config.localsigner
})
}
case SignerType.LEDGER: {
if (config.networkType !== 'ton') {
throw new Error('Ledger signer is only supported for TON network')
}

const transport = await TransportNodeHid.create()

return new LedgerTonSigner({
transport: transport,
logger: options.logger,
...config.ledger
})
}
}
}
4 changes: 4 additions & 0 deletions packages/staking-cli/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { TonNetworkConfig } from '@chorus-one/ton'
import type { SolanaNetworkConfig } from '@chorus-one/solana'
import type { FireblocksSignerConfig } from '@chorus-one/signer-fireblocks'
import type { LocalSignerConfig } from '@chorus-one/signer-local'
import type { LedgerTonSignerConfig } from '@chorus-one/signer-ledger-ton'

interface FireblocksSignerCliConfig extends Omit<FireblocksSignerConfig, 'apiSecretKey' | 'apiKey'> {
apiSecretKeyPath: string
Expand All @@ -34,6 +35,9 @@ export interface Config {
// use local signer (used for testing)
localsigner: LocalSignerCliConfig

// use ledger hardware wallet as signer
ledger: LedgerTonSignerConfig // | LedgerCosmosSignerConfig | ...

// the network type to interact with
networkType: NetworkType

Expand Down
Loading