Skip to content

Commit b1bbd4d

Browse files
2 parents 80a9c3e + 8168145 commit b1bbd4d

File tree

48 files changed

+2291
-290
lines changed

Some content is hidden

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

48 files changed

+2291
-290
lines changed

.github/renovate.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
{
22
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3-
"extends": ["github>BitGo/gha-renovate-bot//presets/onboarding#v1.15.1"],
3+
"extends": ["github>BitGo/gha-renovate-bot//presets/default"],
44
"baseBranches": ["master"],
5-
"enabledManagers": ["github-actions", "regex"],
5+
"enabledManagers": ["github-actions", "regex", "npm"],
6+
"packageRules": [
7+
{
8+
"description": "Disable all npm dependencies by default",
9+
"matchManagers": ["npm"],
10+
"enabled": false
11+
},
12+
{
13+
"description": "Enable updates only for @bitgo/public-types",
14+
"matchPackageNames": ["@bitgo/public-types"],
15+
"enabled": true
16+
}
17+
]
618
}

examples/ts/sol/stake-jito.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require('dotenv').config({ path: '../../.env' });
1616

1717
const AMOUNT_LAMPORTS = 1000;
1818
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
19-
const NETWORK = 'devnet';
19+
const NETWORK = 'testnet';
2020

2121
const bitgo = new BitGoAPI({
2222
accessToken: process.env.TESTNET_ACCESS_TOKEN,

examples/ts/sol/unstake-jito.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Unstakes JitoSOL tokens on Solana devnet.
3+
*
4+
* Copyright 2025, BitGo, Inc. All Rights Reserved.
5+
*/
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, Keypair } from '@solana/web3.js';
11+
import { getStakePoolAccount } from '@solana/spl-stake-pool';
12+
import * as bs58 from 'bs58';
13+
14+
require('dotenv').config({ path: '../../.env' });
15+
16+
const AMOUNT_TOKENS = 100;
17+
const JITO_STAKE_POOL_ADDRESS = 'Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb';
18+
const NETWORK = 'testnet';
19+
// You must find a validator. Try prepareWithdrawAccounts.
20+
21+
const bitgo = new BitGoAPI({
22+
accessToken: process.env.TESTNET_ACCESS_TOKEN,
23+
env: 'test',
24+
});
25+
const coin = coins.get('tsol');
26+
bitgo.register(coin.name, Tsol.createInstance);
27+
28+
async function main() {
29+
const account = getAccount();
30+
const { validatorAddress } = getValidator();
31+
const connection = new Connection(clusterApiUrl(NETWORK), 'confirmed');
32+
const recentBlockhash = await connection.getLatestBlockhash();
33+
const stakePoolAddress = new PublicKey(JITO_STAKE_POOL_ADDRESS);
34+
const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress);
35+
console.info('Validator list account', stakePoolAccount.account.data.validatorList.toBase58());
36+
37+
const transferAuthority = Keypair.generate();
38+
const stakeAccount = Keypair.generate();
39+
console.info('Transfer authority public key:', transferAuthority.publicKey.toBase58());
40+
console.info('Stake account public key:', stakeAccount.publicKey.toBase58());
41+
42+
// Use BitGoAPI to build withdrawStake instruction
43+
const txBuilder = new TransactionBuilderFactory(coin).getStakingDeactivateBuilder();
44+
txBuilder
45+
.amount(`${AMOUNT_TOKENS}`)
46+
.sender(account.publicKey.toBase58())
47+
.stakingAddress(JITO_STAKE_POOL_ADDRESS)
48+
.unstakingAddress(stakeAccount.publicKey.toBase58())
49+
.stakingType(SolStakingTypeEnum.JITO)
50+
.extraParams({
51+
stakePoolData: {
52+
managerFeeAccount: stakePoolAccount.account.data.managerFeeAccount.toBase58(),
53+
poolMint: stakePoolAccount.account.data.poolMint.toBase58(),
54+
validatorListAccount: stakePoolAccount.account.data.validatorList.toBase58(),
55+
},
56+
validatorAddress,
57+
transferAuthorityAddress: transferAuthority.publicKey.toBase58(),
58+
})
59+
.nonce(recentBlockhash.blockhash);
60+
61+
txBuilder.sign({ key: account.secretKey });
62+
txBuilder.sign({ key: bs58.encode(stakeAccount.secretKey) });
63+
txBuilder.sign({ key: bs58.encode(transferAuthority.secretKey) });
64+
65+
const tx = await txBuilder.build();
66+
const serializedTx = tx.toBroadcastFormat();
67+
console.info(serializedTx);
68+
console.info(`Transaction JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`);
69+
70+
// Send transaction
71+
try {
72+
const sig = await connection.sendRawTransaction(Buffer.from(serializedTx, 'base64'));
73+
await connection.confirmTransaction(sig);
74+
console.log(`${AMOUNT_TOKENS} tokens withdrawn`, sig);
75+
} catch (e) {
76+
console.log('Error sending transaction');
77+
console.error(e);
78+
}
79+
}
80+
81+
const getAccount = () => {
82+
const publicKey = process.env.ACCOUNT_PUBLIC_KEY;
83+
const secretKey = process.env.ACCOUNT_SECRET_KEY;
84+
if (publicKey === undefined || secretKey === undefined) {
85+
const { publicKey, secretKey } = Keypair.generate();
86+
console.log('# Here is a new account to save into your .env file.');
87+
console.log(`ACCOUNT_PUBLIC_KEY=${publicKey.toBase58()}`);
88+
console.log(`ACCOUNT_SECRET_KEY=${bs58.encode(secretKey)}`);
89+
throw new Error('Missing account information');
90+
}
91+
92+
return {
93+
publicKey: new PublicKey(publicKey),
94+
secretKey,
95+
secretKeyArray: new Uint8Array(bs58.decode(secretKey)),
96+
};
97+
};
98+
99+
const getValidator = () => {
100+
const validatorAddress = process.env.VALIDATOR_PUBLIC_KEY;
101+
if (validatorAddress === undefined) {
102+
console.log('# You must select a validator, then define the entry below');
103+
console.log('VALIDATOR_PUBLIC_KEY=');
104+
throw new Error('Missing validator address');
105+
}
106+
return { validatorAddress };
107+
};
108+
109+
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/bitgo/test/v2/fixtures/staking/stakingWallet.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,42 @@ export default {
7777
},
7878
senderWalletId: 'btcDescriptorWalletId',
7979
},
80+
81+
matchingDataBuildParams: {
82+
memo: 'matching-memo',
83+
gasLimit: 200000,
84+
type: 'pay',
85+
solInstructions: [{ programId: 'Stake111', keys: [], data: 'delegate' }],
86+
aptosCustomTransactionParams: { moduleName: '0x1::staking', functionName: 'stake' },
87+
},
88+
89+
mismatchMemoBuildParams: {
90+
memo: 'user-memo',
91+
},
92+
93+
mismatchGasLimitBuildParams: {
94+
gasLimit: 100000,
95+
},
96+
97+
mismatchTypeBuildParams: {
98+
type: 'stake',
99+
},
100+
101+
mismatchSolInstructionsBuildParams: {
102+
solInstructions: [
103+
{
104+
programId: 'UserStakeProgram1111111111111111111111111111',
105+
keys: [{ pubkey: 'user-key', isSigner: true, isWritable: true }],
106+
data: 'user-delegate-data',
107+
},
108+
],
109+
},
110+
111+
mismatchAptosParamsBuildParams: {
112+
aptosCustomTransactionParams: {
113+
moduleName: '0x1::staking',
114+
functionName: 'stake',
115+
functionArguments: ['10000'],
116+
},
117+
},
80118
};

modules/express/src/clientRoutes.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
15581558
router.post('express.decrypt', [prepareBitGo(config), typedPromiseWrapper(handleDecrypt)]);
15591559
router.post('express.encrypt', [prepareBitGo(config), typedPromiseWrapper(handleEncrypt)]);
15601560
router.post('express.verifyaddress', [prepareBitGo(config), typedPromiseWrapper(handleVerifyAddress)]);
1561+
router.post('express.lightning.initWallet', [prepareBitGo(config), typedPromiseWrapper(handleInitLightningWallet)]);
15611562
app.post(
15621563
'/api/v[12]/calculateminerfeeinfo',
15631564
parseBody,
@@ -1777,12 +1778,6 @@ export function setupEnclavedExpressRoutes(app: express.Application, config: Con
17771778
}
17781779

17791780
export function setupLightningSignerNodeRoutes(app: express.Application, config: Config): void {
1780-
app.post(
1781-
'/api/v2/:coin/wallet/:id/initwallet',
1782-
parseBody,
1783-
prepareBitGo(config),
1784-
promiseWrapper(handleInitLightningWallet)
1785-
);
17861781
app.post(
17871782
'/api/v2/:coin/wallet/:id/signermacaroon',
17881783
parseBody,

modules/express/src/lightning/lightningSignerRoutes.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,10 @@ import {
1414
import * as utxolib from '@bitgo/utxo-lib';
1515
import { Buffer } from 'buffer';
1616

17-
import {
18-
CreateSignerMacaroonRequest,
19-
GetWalletStateResponse,
20-
InitLightningWalletRequest,
21-
UnlockLightningWalletRequest,
22-
} from './codecs';
17+
import { CreateSignerMacaroonRequest, GetWalletStateResponse, UnlockLightningWalletRequest } from './codecs';
2318
import { LndSignerClient } from './lndSignerClient';
2419
import { ApiResponseError } from '../errors';
20+
import { ExpressApiRouteRequest } from '../typedRoutes/api';
2521

2622
type Decrypt = (params: { input: string; password: string }) => string;
2723

@@ -61,29 +57,16 @@ function getMacaroonRootKey(passphrase: string, nodeAuthEncryptedPrv: string, de
6157
/**
6258
* Handle the request to initialise remote signer LND for a wallet.
6359
*/
64-
export async function handleInitLightningWallet(req: express.Request): Promise<unknown> {
60+
export async function handleInitLightningWallet(
61+
req: ExpressApiRouteRequest<'express.lightning.initWallet', 'post'>
62+
): Promise<unknown> {
6563
const bitgo = req.bitgo;
66-
const coinName = req.params.coin;
64+
const { coin: coinName, walletId, passphrase, expressHost } = req.decoded;
6765
if (!isLightningCoinName(coinName)) {
6866
throw new ApiResponseError(`Invalid coin ${coinName}. This is not a lightning coin.`, 400);
6967
}
7068
const coin = bitgo.coin(coinName);
7169

72-
const walletId = req.params.id;
73-
if (typeof walletId !== 'string') {
74-
throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400);
75-
}
76-
77-
const { passphrase, expressHost } = decodeOrElse(
78-
InitLightningWalletRequest.name,
79-
InitLightningWalletRequest,
80-
req.body,
81-
(_) => {
82-
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
83-
throw new ApiResponseError('Invalid request body to initialize lightning wallet', 400);
84-
}
85-
);
86-
8770
const wallet = await coin.wallets().get({ id: walletId, includeBalance: false });
8871
if (wallet.subType() !== 'lightningSelfCustody') {
8972
throw new ApiResponseError(`not a self custodial lighting wallet ${walletId}`, 400);

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PostSimpleCreate } from './v1/simpleCreate';
1313
import { PutPendingApproval } from './v1/pendingApproval';
1414
import { PostSignTransaction } from './v1/signTransaction';
1515
import { PostKeychainLocal } from './v2/keychainLocal';
16+
import { PostLightningInitWallet } from './v2/lightningInitWallet';
1617
import { PostVerifyCoinAddress } from './v2/verifyAddress';
1718

1819
export const ExpressApi = apiSpec({
@@ -49,6 +50,9 @@ export const ExpressApi = apiSpec({
4950
'express.keychain.local': {
5051
post: PostKeychainLocal,
5152
},
53+
'express.lightning.initWallet': {
54+
post: PostLightningInitWallet,
55+
},
5256
'express.verifycoinaddress': {
5357
post: PostVerifyCoinAddress,
5458
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Path parameters for initializing a Lightning wallet
7+
* @property {string} coin - A lightning coin name (e.g, lnbtc, tlnbtc).
8+
* @property {string} walletId - The ID of the wallet.
9+
*/
10+
export const LightningInitWalletParams = {
11+
coin: t.string,
12+
walletId: t.string,
13+
} as const;
14+
15+
/**
16+
* Request body for initializing a Lightning wallet
17+
*/
18+
export const LightningInitWalletBody = {
19+
/** Passphrase to encrypt the admin macaroon of the signer node. */
20+
passphrase: t.string,
21+
/** Optional hostname or IP address to set the `expressHost` field of the wallet. */
22+
expressHost: optional(t.string),
23+
} as const;
24+
25+
/**
26+
* Response for initializing a Lightning wallet
27+
*/
28+
export const LightningInitWalletResponse = {
29+
/** Returns the updated wallet. On success, the wallet's `coinSpecific` will include the encrypted admin macaroon for the Lightning signer node. */
30+
200: t.unknown,
31+
/** BitGo Express error payload when initialization cannot proceed (for example: invalid coin, unsupported environment, wallet not in an initializable state). */
32+
400: BitgoExpressError,
33+
} as const;
34+
35+
/**
36+
* Lightning - This is only used for self-custody lightning. Initialize a newly created Lightning Network Daemon (LND) for the first time.
37+
* Returns the updated wallet with the encrypted admin macaroon in the `coinSpecific` response field.
38+
*
39+
* @operationId express.lightning.initWallet
40+
*/
41+
export const PostLightningInitWallet = httpRoute({
42+
path: '/api/v2/:coin/wallet/:walletId/initwallet',
43+
method: 'POST',
44+
request: httpRequest({ params: LightningInitWalletParams, body: LightningInitWalletBody }),
45+
response: LightningInitWalletResponse,
46+
});
47+
48+
export type PostLightningInitWallet = typeof PostLightningInitWallet;

0 commit comments

Comments
 (0)