Skip to content

Commit 9f073c6

Browse files
2 parents 2940920 + 8874bf0 commit 9f073c6

File tree

80 files changed

+4003
-529
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+4003
-529
lines changed

examples/ts/sol/stake-jito.ts

Lines changed: 65 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,106 +3,110 @@
33
*
44
* Copyright 2025, BitGo, Inc. All Rights Reserved.
55
*/
6-
import { BitGoAPI } from '@bitgo/sdk-api'
7-
import { TransactionBuilderFactory, Tsol } from '@bitgo/sdk-coin-sol'
8-
import { coins } from '@bitgo/statics'
9-
import { Connection, PublicKey, clusterApiUrl, Transaction, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"
10-
import { getStakePoolAccount, updateStakePool } from '@solana/spl-stake-pool'
6+
import { SolStakingTypeEnum } from '@bitgo/public-types';
7+
import { BitGoAPI } from '@bitgo/sdk-api';
8+
import { TransactionBuilderFactory, Tsol } from '@bitgo/sdk-coin-sol';
9+
import { coins } from '@bitgo/statics';
10+
import { Connection, PublicKey, clusterApiUrl, Transaction, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';
11+
import { getStakePoolAccount, updateStakePool } from '@solana/spl-stake-pool';
12+
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
1113
import * as bs58 from 'bs58';
1214

13-
require('dotenv').config({ path: '../../.env' })
15+
require('dotenv').config({ path: '../../.env' });
1416

15-
const AMOUNT_LAMPORTS = 1000
16-
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb'
17-
const NETWORK = 'devnet'
17+
const AMOUNT_LAMPORTS = 1000;
18+
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
19+
const NETWORK = 'devnet';
1820

1921
const bitgo = new BitGoAPI({
2022
accessToken: process.env.TESTNET_ACCESS_TOKEN,
2123
env: 'test',
22-
})
23-
const coin = coins.get("tsol")
24-
bitgo.register(coin.name, Tsol.createInstance)
24+
});
25+
const coin = coins.get('tsol');
26+
bitgo.register(coin.name, Tsol.createInstance);
2527

2628
async function main() {
27-
const account = getAccount()
28-
const connection = new Connection(clusterApiUrl(NETWORK), 'confirmed')
29-
const recentBlockhash = await connection.getLatestBlockhash()
30-
const stakePoolAccount = await getStakePoolAccount(connection, new PublicKey(JITO_STAKE_POOL_ADDRESS))
31-
29+
const account = getAccount();
30+
const connection = new Connection(clusterApiUrl(NETWORK), 'confirmed');
31+
const recentBlockhash = await connection.getLatestBlockhash();
32+
const stakePoolAccount = await getStakePoolAccount(connection, new PublicKey(JITO_STAKE_POOL_ADDRESS));
33+
const associatedTokenAddress = getAssociatedTokenAddressSync(
34+
stakePoolAccount.account.data.poolMint,
35+
account.publicKey
36+
);
37+
const associatedTokenAccountExists = !!(await connection.getAccountInfo(associatedTokenAddress));
3238

3339
// Account should have sufficient balance
34-
const accountBalance = await connection.getBalance(account.publicKey)
40+
const accountBalance = await connection.getBalance(account.publicKey);
3541
if (accountBalance < 0.1 * LAMPORTS_PER_SOL) {
36-
console.info(`Your account balance is ${accountBalance / LAMPORTS_PER_SOL} SOL, requesting airdrop`)
37-
const sig = await connection.requestAirdrop(account.publicKey, 2 * LAMPORTS_PER_SOL)
38-
await connection.confirmTransaction(sig)
39-
console.info(`Airdrop successful: ${sig}`)
42+
console.info(`Your account balance is ${accountBalance / LAMPORTS_PER_SOL} SOL, requesting airdrop`);
43+
const sig = await connection.requestAirdrop(account.publicKey, 2 * LAMPORTS_PER_SOL);
44+
await connection.confirmTransaction(sig);
45+
console.info(`Airdrop successful: ${sig}`);
4046
}
4147

4248
// Stake pool should be up to date
43-
const epochInfo = await connection.getEpochInfo()
49+
const epochInfo = await connection.getEpochInfo();
4450
if (stakePoolAccount.account.data.lastUpdateEpoch.ltn(epochInfo.epoch)) {
45-
console.info('Stake pool is out of date.')
46-
const usp = await updateStakePool(connection, stakePoolAccount)
47-
const tx = new Transaction()
48-
tx.add(...usp.updateListInstructions, ...usp.finalInstructions)
49-
const signer = Keypair.fromSecretKey(account.secretKeyArray)
50-
const sig = await connection.sendTransaction(tx, [signer])
51-
await connection.confirmTransaction(sig)
52-
console.info(`Stake pool updated: ${sig}`)
51+
console.info('Stake pool is out of date.');
52+
const usp = await updateStakePool(connection, stakePoolAccount);
53+
const tx = new Transaction();
54+
tx.add(...usp.updateListInstructions, ...usp.finalInstructions);
55+
const signer = Keypair.fromSecretKey(account.secretKeyArray);
56+
const sig = await connection.sendTransaction(tx, [signer]);
57+
await connection.confirmTransaction(sig);
58+
console.info(`Stake pool updated: ${sig}`);
5359
}
5460

5561
// Use BitGoAPI to build depositSol instruction
56-
const txBuilder = new TransactionBuilderFactory(coin).getStakingActivateBuilder()
62+
const txBuilder = new TransactionBuilderFactory(coin).getStakingActivateBuilder();
5763
txBuilder
5864
.amount(`${AMOUNT_LAMPORTS}`)
5965
.sender(account.publicKey.toBase58())
6066
.stakingAddress(JITO_STAKE_POOL_ADDRESS)
6167
.validator(JITO_STAKE_POOL_ADDRESS)
62-
.stakingTypeParams({
63-
type: 'JITO',
68+
.stakingType(SolStakingTypeEnum.JITO)
69+
.extraParams({
6470
stakePoolData: {
65-
managerFeeAccount: stakePoolAccount.account.data.managerFeeAccount.toString(),
66-
poolMint: stakePoolAccount.account.data.poolMint.toString(),
67-
reserveStake: stakePoolAccount.account.data.toString(),
68-
}
71+
managerFeeAccount: stakePoolAccount.account.data.managerFeeAccount.toBase58(),
72+
poolMint: stakePoolAccount.account.data.poolMint.toBase58(),
73+
reserveStake: stakePoolAccount.account.data.reserveStake.toBase58(),
74+
},
75+
createAssociatedTokenAccount: !associatedTokenAccountExists,
6976
})
70-
.nonce(recentBlockhash.blockhash)
71-
txBuilder.sign({ key: account.secretKey })
72-
const tx = await txBuilder.build()
73-
const serializedTx = tx.toBroadcastFormat()
74-
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`)
77+
.nonce(recentBlockhash.blockhash);
78+
txBuilder.sign({ key: account.secretKey });
79+
const tx = await txBuilder.build();
80+
const serializedTx = tx.toBroadcastFormat();
81+
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`);
7582

7683
// Send transaction
7784
try {
78-
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'))
79-
await connection.confirmTransaction(sig)
80-
console.log(`${AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL deposited`, sig)
85+
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'));
86+
await connection.confirmTransaction(sig);
87+
console.log(`${AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL deposited`, sig);
8188
} catch (e) {
82-
console.log('Error sending transaction')
83-
console.error(e)
84-
if (e.transactionMessage === 'Transaction simulation failed: Error processing Instruction 0: Provided owner is not allowed') {
85-
console.error('If you successfully staked JitoSOL once, you cannot stake again.')
86-
}
89+
console.log('Error sending transaction');
90+
console.error(e);
8791
}
8892
}
8993

9094
const getAccount = () => {
91-
const publicKey = process.env.ACCOUNT_PUBLIC_KEY
92-
const secretKey = process.env.ACCOUNT_SECRET_KEY
95+
const publicKey = process.env.ACCOUNT_PUBLIC_KEY;
96+
const secretKey = process.env.ACCOUNT_SECRET_KEY;
9397
if (publicKey === undefined || secretKey === undefined) {
94-
const { publicKey, secretKey } = Keypair.generate()
95-
console.log('# Here is a new account to save into your .env file.')
96-
console.log(`ACCOUNT_PUBLIC_KEY=${publicKey.toBase58()}`)
97-
console.log(`ACCOUNT_SECRET_KEY=${bs58.encode(secretKey)}`)
98-
throw new Error("Missing account information")
98+
const { publicKey, secretKey } = Keypair.generate();
99+
console.log('# Here is a new account to save into your .env file.');
100+
console.log(`ACCOUNT_PUBLIC_KEY=${publicKey.toBase58()}`);
101+
console.log(`ACCOUNT_SECRET_KEY=${bs58.encode(secretKey)}`);
102+
throw new Error('Missing account information');
99103
}
100104

101105
return {
102106
publicKey: new PublicKey(publicKey),
103107
secretKey,
104108
secretKeyArray: new Uint8Array(bs58.decode(secretKey)),
105-
}
106-
}
109+
};
110+
};
107111

108-
main().catch((e) => console.error(e))
112+
main().catch((e) => console.error(e));

modules/abstract-lightning/src/codecs/api/wallet.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ export const WatchOnlyAccount = t.type({
5252

5353
export type WatchOnlyAccount = t.TypeOf<typeof WatchOnlyAccount>;
5454

55-
export const WatchOnly = t.type({
56-
master_key_birthday_timestamp: t.string,
57-
master_key_fingerprint: t.string,
58-
accounts: t.array(WatchOnlyAccount),
59-
});
55+
export const WatchOnly = t.intersection([
56+
t.type({
57+
accounts: t.array(WatchOnlyAccount),
58+
}),
59+
t.partial({
60+
master_key_birthday_timestamp: t.string,
61+
master_key_fingerprint: t.string,
62+
}),
63+
]);
6064

6165
export type WatchOnly = t.TypeOf<typeof WatchOnly>;
6266

modules/abstract-lightning/src/lightning/lightningUtils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,18 @@ function convertXpubPrefix(xpub: string, purpose: ExtendedKeyPurpose, isMainnet:
154154
/**
155155
* Derives watch-only accounts from the master HD node for the given purposes and network.
156156
*/
157-
function deriveWatchOnlyAccounts(masterHDNode: utxolib.BIP32Interface, isMainnet: boolean): WatchOnlyAccount[] {
157+
export function deriveWatchOnlyAccounts(
158+
masterHDNode: utxolib.BIP32Interface,
159+
isMainnet: boolean,
160+
params: { onlyAddressCreationAccounts?: boolean } = { onlyAddressCreationAccounts: false }
161+
): WatchOnlyAccount[] {
158162
// https://github.com/lightningnetwork/lnd/blob/master/docs/remote-signing.md#required-accounts
159163
if (masterHDNode.isNeutered()) {
160164
throw new Error('masterHDNode must not be neutered');
161165
}
162-
163-
const purposes = [PURPOSE_WRAPPED_P2WKH, PURPOSE_P2WKH, PURPOSE_P2TR, PURPOSE_ALL_OTHERS] as const;
164-
166+
const purposes = params.onlyAddressCreationAccounts
167+
? ([PURPOSE_WRAPPED_P2WKH, PURPOSE_P2WKH, PURPOSE_P2TR] as const)
168+
: ([PURPOSE_WRAPPED_P2WKH, PURPOSE_P2WKH, PURPOSE_P2TR, PURPOSE_ALL_OTHERS] as const);
165169
return purposes.flatMap((purpose) => {
166170
const maxAccount = purpose === PURPOSE_ALL_OTHERS ? 255 : 0;
167171
const coinType = purpose !== PURPOSE_ALL_OTHERS || isMainnet ? 0 : 1;

modules/abstract-substrate/src/lib/utils.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { hexToU8a, isHex, u8aToHex, u8aToU8a } from '@polkadot/util';
88
import { base64Decode, signatureVerify } from '@polkadot/util-crypto';
99
import { UnsignedTransaction } from '@substrate/txwrapper-core';
1010
import { DecodedSignedTx, DecodedSigningPayload, TypeRegistry } from '@substrate/txwrapper-core/lib/types';
11-
import { construct } from '@substrate/txwrapper-polkadot';
11+
import { construct, decode } from '@substrate/txwrapper-polkadot';
1212
import bs58 from 'bs58';
1313
import base32 from 'hi-base32';
1414
import nacl from 'tweetnacl';
@@ -30,6 +30,7 @@ import {
3030
BatchArgs,
3131
MoveStakeArgs,
3232
} from './iface';
33+
import { SingletonRegistry } from './singletonRegistry';
3334

3435
export class Utils implements BaseUtils {
3536
/** @inheritdoc */
@@ -317,6 +318,31 @@ export class Utils implements BaseUtils {
317318
getMaterial(networkType: NetworkType): Material {
318319
throw new Error('Method not implemented.');
319320
}
321+
322+
/**
323+
* Decodes a substrate transaction from raw transaction hex
324+
*
325+
* @param {string} txHex - The raw transaction hex string to decode (signed or unsigned)
326+
* @param {Material} material - Network material containing metadata and chain information
327+
* @param {boolean} [isImmortalEra] - Whether the transaction uses immortal era (optional)
328+
* @returns {DecodedSignedTx | DecodedSigningPayload} The decoded transaction object
329+
*/
330+
decodeTransaction(txHex: string, material: Material, isImmortalEra = false): DecodedSignedTx | DecodedSigningPayload {
331+
try {
332+
const registry = SingletonRegistry.getInstance(material);
333+
334+
// Attempt to decode as a signed transaction or unsigned transaction
335+
const decoded = decode(txHex, {
336+
metadataRpc: material.metadata,
337+
registry,
338+
isImmortalEra,
339+
});
340+
341+
return decoded;
342+
} catch (error) {
343+
throw new Error(`Failed to decode transaction: ${error}`);
344+
}
345+
}
320346
}
321347

322348
const utils = new Utils();

modules/bitgo/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@
147147
"@types/lodash": "^4.14.121",
148148
"aws-sdk": "^2.1155.0",
149149
"io-ts-types": "^0.5.16",
150-
"karma": "^6.4.2",
151-
"karma-chrome-launcher": "^3.1.0",
152-
"karma-jasmine": "^4.0.0",
153-
"karma-typescript": "^5.0.3",
150+
"karma": "6.4.4",
151+
"karma-chrome-launcher": "3.2.0",
152+
"karma-jasmine": "5.1.0",
153+
"karma-typescript": "5.5.4",
154154
"keccak": "3.0.3",
155155
"libsodium-wrappers-sumo": "^0.7.9",
156156
"puppeteer": "2.1.1",

modules/bitgo/src/v2/coinFactory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
VetTokenConfig,
3838
TaoTokenConfig,
3939
PolyxTokenConfig,
40+
JettonTokenConfig,
4041
} from '@bitgo/statics';
4142
import {
4243
Ada,
@@ -99,6 +100,7 @@ import {
99100
Injective,
100101
Iota,
101102
Islm,
103+
JettonToken,
102104
Lnbtc,
103105
Ltc,
104106
Mon,
@@ -528,6 +530,10 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
528530
({ name, coinConstructor }) => coinFactory.register(name, coinConstructor)
529531
);
530532

533+
JettonToken.createTokenConstructors([...tokens.bitcoin.ton.tokens, ...tokens.testnet.ton.tokens]).forEach(
534+
({ name, coinConstructor }) => coinFactory.register(name, coinConstructor)
535+
);
536+
531537
VetToken.createTokenConstructors().forEach(({ name, coinConstructor }) =>
532538
coinFactory.register(name, coinConstructor)
533539
);
@@ -971,6 +977,9 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor |
971977
case 'flr':
972978
case 'tflr':
973979
return FlrToken.createTokenConstructor(tokenConfig as EthLikeTokenConfig);
980+
case 'ton':
981+
case 'tton':
982+
return JettonToken.createTokenConstructor(tokenConfig as JettonTokenConfig);
974983
default:
975984
return undefined;
976985
}

modules/bitgo/src/v2/coins/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import { Stx, Tstx, Sip10Token } from '@bitgo/sdk-coin-stx';
6060
import { Sui, Tsui, SuiToken } from '@bitgo/sdk-coin-sui';
6161
import { Tao, Ttao, TaoToken } from '@bitgo/sdk-coin-tao';
6262
import { Tia, Ttia } from '@bitgo/sdk-coin-tia';
63-
import { Ton, Tton } from '@bitgo/sdk-coin-ton';
63+
import { Ton, Tton, JettonToken } from '@bitgo/sdk-coin-ton';
6464
import { Trx, Ttrx } from '@bitgo/sdk-coin-trx';
6565
import { StellarToken, Txlm, Xlm } from '@bitgo/sdk-coin-xlm';
6666
import { Vet, Tvet, VetToken } from '@bitgo/sdk-coin-vet';
@@ -131,7 +131,7 @@ export { Stx, Tstx, Sip10Token };
131131
export { Sui, Tsui, SuiToken };
132132
export { Tao, Ttao, TaoToken };
133133
export { Tia, Ttia };
134-
export { Ton, Tton };
134+
export { Ton, Tton, JettonToken };
135135
export { Bld, Tbld };
136136
export { Sei, Tsei };
137137
export { Injective, Tinjective };

modules/bitgo/test/browser/browser.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('Coins', () => {
5757
EthLikeErc20Token: 1,
5858
HashToken: 1,
5959
FlrToken: 1,
60+
JettonToken: 1,
6061
};
6162
Object.keys(BitGoJS.Coin)
6263
.filter((coinName) => !excludedKeys[coinName])

modules/bitgo/test/v2/unit/staking/stakingWalletNonTSS.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ describe('non-TSS Staking Wallet', function () {
400400
await topethWctStakingWallet.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction);
401401
});
402402

403-
it('should log an error if a transaction is failed to be validated transaction if unsigned transaction does not match the staking transaction', async function () {
403+
it('should throw an error if unsigned transaction does not match the staking transaction', async function () {
404404
const unsignedTransaction: PrebuildTransactionResult = {
405405
walletId: topethWctStakingWallet.walletId,
406406
...opethFixtures.unsignedStakingTransaction,
@@ -409,8 +409,6 @@ describe('non-TSS Staking Wallet', function () {
409409
} as PrebuildTransactionResult;
410410
const stakingTransaction: StakingTransaction = opethFixtures.updatedStakingRequest;
411411

412-
const consoleStub = sinon.stub(console, 'error');
413-
414412
nock(microservicesUri)
415413
.get(
416414
`/api/staking/v1/${topethWctStakingWallet.coin}/wallets/${topethWctStakingWallet.walletId}/requests/${stakingTransaction.stakingRequestId}/transactions/${stakingTransaction.id}`
@@ -426,11 +424,14 @@ describe('non-TSS Staking Wallet', function () {
426424
.post(`/api/v2/topeth/wallet/${topethWctStakingWallet.walletId}/tx/build`)
427425
.reply(200, unsignedTransaction);
428426

429-
await topethWctStakingWallet.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction);
427+
const expectedErrorMessage =
428+
'Staking transaction validation failed before signing: ' +
429+
'Unexpected recipient address found in built transaction: 0x86bb6dca2cd6f9a0189c478bbb8f7ee2fef07c89; ' +
430+
'Expected recipient address not found in built transaction: 0x75bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78';
430431

431-
consoleStub.calledWith(
432-
'Invalid recipient address: 0x86bb6dca2cd6f9a0189c478bbb8f7ee2fef07c89, Missing recipient address(es): 0x75bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78'
433-
);
432+
await topethWctStakingWallet
433+
.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction)
434+
.should.be.rejectedWith(expectedErrorMessage);
434435
});
435436
});
436437
});

0 commit comments

Comments
 (0)