@tetherto/wdk-wallet-ton
Version:
A simple package to manage BIP-32 wallets for the ton blockchain.
236 lines (190 loc) • 6.67 kB
JavaScript
// Copyright 2024 Tether Operations Limited
//
// 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.
'use strict'
import { sign, signVerify } from '@ton/crypto'
import { SendMode } from '@ton/ton'
import nacl from 'tweetnacl'
import HDKey from 'micro-key-producer/slip10.js'
// eslint-disable-next-line camelcase
import { sodium_memzero } from 'sodium-universal'
import * as bip39 from 'bip39'
import WalletAccountReadOnlyTon from './wallet-account-read-only-ton.js'
/** @typedef {import('@ton/ton').MessageRelaxed} MessageRelaxed */
/** @typedef {import('@ton/ton').Transaction} TonTransactionReceipt */
/** @typedef {import('@tetherto/wdk-wallet').IWalletAccount} IWalletAccount */
/** @typedef {import('@tetherto/wdk-wallet').KeyPair} KeyPair */
/** @typedef {import('@tetherto/wdk-wallet').TransactionResult} TransactionResult */
/** @typedef {import('@tetherto/wdk-wallet').TransferOptions} TransferOptions */
/** @typedef {import('@tetherto/wdk-wallet').TransferResult} TransferResult */
/** @typedef {import('./wallet-account-read-only-ton.js').TonTransaction} TonTransaction */
/** @typedef {import('./wallet-account-read-only-ton.js').TonClientConfig} TonClientConfig */
/** @typedef {import('./wallet-account-read-only-ton.js').TonWalletConfig} TonWalletConfig */
const BIP_44_TON_DERIVATION_PATH_PREFIX = "m/44'/607'"
function derivePath (seed, path) {
const hdKey = HDKey.fromMasterSeed(seed)
const { privateKey } = hdKey.derive(path, true)
const keyPair = nacl.sign.keyPair.fromSeed(privateKey)
sodium_memzero(privateKey)
return keyPair
}
/** @implements {IWalletAccount} */
export default class WalletAccountTon extends WalletAccountReadOnlyTon {
/**
* Creates a new ton wallet account.
*
* @param {string | Uint8Array} seed - The wallet's [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) seed phrase.
* @param {string} path - The BIP-44 derivation path (e.g. "0'/0/0").
* @param {TonWalletConfig} [config] - The configuration object.
*/
constructor (seed, path, config = { }) {
if (typeof seed === 'string') {
if (!bip39.validateMnemonic(seed)) {
throw new Error('The seed phrase is invalid.')
}
seed = bip39.mnemonicToSeedSync(seed)
}
path = BIP_44_TON_DERIVATION_PATH_PREFIX + '/' + path
const keyPair = derivePath(seed, path)
super(keyPair.publicKey, config)
/**
* The wallet account configuration.
*
* @protected
* @type {TonWalletConfig}
*/
this._config = config
/** @private */
this._path = path
/** @private */
this._keyPair = keyPair
}
/**
* The derivation path's index of this account.
*
* @type {number}
*/
get index () {
return +this._path.split('/').pop()
}
/**
* The derivation path of this account (see [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)).
*
* @type {string}
*/
get path () {
return this._path
}
/**
* The account's key pair.
*
* @type {KeyPair}
*/
get keyPair () {
return {
publicKey: this._keyPair.publicKey,
privateKey: this._keyPair.secretKey
}
}
/**
* Signs a message.
*
* @param {string} message - The message to sign.
* @returns {Promise<string>} The message's signature.
*/
async sign (message) {
const _message = Buffer.from(message)
return sign(_message, this._keyPair.secretKey)
.toString('hex')
}
/**
* Verifies a message's signature.
*
* @param {string} message - The original message.
* @param {string} signature - The signature to verify.
* @returns {Promise<boolean>} True if the signature is valid.
*/
async verify (message, signature) {
const _message = Buffer.from(message)
const _signature = Buffer.from(signature, 'hex')
return signVerify(_message, _signature, this._keyPair.publicKey)
}
/**
* Sends a transaction.
*
* @param {TonTransaction} tx - The transaction.
* @returns {Promise<TransactionResult>} The transaction's result.
*/
async sendTransaction (tx) {
if (!this._tonClient) {
throw new Error('The wallet must be connected to ton center to send transactions.')
}
const message = await this._getTransactionMessage(tx)
const transfer = await this._getTransfer(message)
const fee = await this._getTransferFee(transfer)
await this._contract.send(transfer)
return {
hash: transfer.hash().toString('hex'),
fee
}
}
/**
* Transfers a token to another address.
*
* @param {TransferOptions} options - The transfer's options.
* @returns {Promise<TransferResult>} The transfer's result.
*/
async transfer (options) {
if (!this._tonClient) {
throw new Error('The wallet must be connected to ton center to transfer tokens.')
}
const message = await this._getTokenTransferMessage(options)
const transfer = await this._getTransfer(message)
const fee = await this._getTransferFee(transfer)
// eslint-disable-next-line eqeqeq
if (this._config.transferMaxFee != undefined && fee >= this._config.transferMaxFee) {
throw new Error('Exceeded maximum fee cost for transfer operations.')
}
await this._contract.send(transfer)
return {
hash: transfer.hash().toString('hex'),
fee
}
}
/**
* Returns a read-only copy of the account.
*
* @returns {Promise<WalletAccountReadOnlyTon>} The read-only account.
*/
async toReadOnlyAccount () {
const readOnlyAccount = new WalletAccountReadOnlyTon(this._keyPair.publicKey, this._config)
return readOnlyAccount
}
/**
* Disposes the wallet account, erasing the private key from the memory.
*/
dispose () {
sodium_memzero(this._keyPair.secretKey)
this._keyPair.secretKey = undefined
}
async _getTransfer (message) {
const seqno = await this._contract.getSeqno()
const transfer = this._contract.createTransfer({
secretKey: this._keyPair.secretKey,
sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS,
messages: [message],
seqno
})
return transfer
}
}