Skip to content
Open
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
15 changes: 15 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,20 @@ export async function handleV2IsWalletAddress(
return await wallet.baseCoin.isWalletAddress(req.decoded as any);
}

/**
* handle v2 deriveAddress - locally derive and return a wallet receive address from a
* derivation path, using public key material only.
*
* Offline by design: operates purely on the request body (keychains + chain/index), with no
* `wallets().get` lookup and no network access. The inverse of {@link handleV2IsWalletAddress}.
* @param req
*/
export async function handleV2DeriveAddress(req: ExpressApiRouteRequest<'express.v2.address.derive', 'post'>) {
const bitgo = req.bitgo;
const coin = bitgo.coin(req.decoded.coin);
return await coin.deriveAddress(req.decoded as any);
}

/**
* handle v2 approve transaction
* @param req
Expand Down Expand Up @@ -1963,6 +1977,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
prepareBitGo(config),
typedPromiseWrapper(handleV2IsWalletAddress),
]);
router.post('express.v2.address.derive', [prepareBitGo(config), typedPromiseWrapper(handleV2DeriveAddress)]);

router.post('express.wallet.share', [prepareBitGo(config), typedPromiseWrapper(handleV2ShareWallet)]);
app.post(
Expand Down
9 changes: 9 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { PostWalletEnableTokens } from './v2/walletEnableTokens';
import { PostWalletSweep } from './v2/walletSweep';
import { PostWalletAccelerateTx } from './v2/walletAccelerateTx';
import { PostIsWalletAddress } from './v2/isWalletAddress';
import { PostDeriveAddress } from './v2/deriveAddress';
import { GetAccountResources } from './v2/accountResources';
import { GetResourceDelegations } from './v2/resourceDelegations';
import { PostDelegateResources } from './v2/delegateResources';
Expand Down Expand Up @@ -235,6 +236,12 @@ export const ExpressV2WalletIsWalletAddressApiSpec = apiSpec({
},
});

export const ExpressV2AddressDeriveApiSpec = apiSpec({
'express.v2.address.derive': {
post: PostDeriveAddress,
},
});

export const ExpressV2WalletSendManyApiSpec = apiSpec({
'express.wallet.sendmany': {
post: PostSendMany,
Expand Down Expand Up @@ -399,6 +406,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressWalletFanoutUnspentsApiSpec &
typeof ExpressV2WalletCreateAddressApiSpec &
typeof ExpressV2WalletIsWalletAddressApiSpec &
typeof ExpressV2AddressDeriveApiSpec &
typeof ExpressKeychainLocalApiSpec &
typeof ExpressKeychainChangePasswordApiSpec &
typeof ExpressLightningWalletPaymentApiSpec &
Expand Down Expand Up @@ -444,6 +452,7 @@ export const ExpressApi: ExpressApi = {
...ExpressV2WalletCreateAddressApiSpec,
...ExpressV2WalletConsolidateAccountApiSpec,
...ExpressV2WalletIsWalletAddressApiSpec,
...ExpressV2AddressDeriveApiSpec,
...ExpressKeychainLocalApiSpec,
...ExpressKeychainChangePasswordApiSpec,
...ExpressLightningWalletPaymentApiSpec,
Expand Down
103 changes: 103 additions & 0 deletions modules/express/src/typedRoutes/api/v2/deriveAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { CreateAddressFormat } from '../../schemas/address';

/**
* Path parameters for locally deriving a wallet address
*/
export const DeriveAddressParams = {
/** Blockchain identifier (e.g., 'btc', 'eth', 'tbtc', 'teth', 'sol') */
coin: t.string,
} as const;

/**
* A keychain entry for local derivation. Public key material only — no private keys.
* Modelled as a union so a keychain must carry at least one of `pub` / `commonKeychain`:
* - `pub` (xpub) for BIP32 multisig coins (UTXO, legacy EVM)
* - `commonKeychain` for TSS/MPC coins (SOL, EVM MPC) — identical across keychains
*
* (A keychain may legitimately carry both; TSS keychains commonly do.)
*/
export const DeriveAddressKeychainCodec = t.union([t.type({ pub: t.string }), t.type({ commonKeychain: t.string })]);

/** A non-negative integer — used for derivation `index` and `chain` (both are always >= 0). */
export const NonNegativeInteger = t.refinement(
t.number,
(n): boolean => Number.isInteger(n) && n >= 0,
'NonNegativeInteger'
);

/**
* Request body for locally deriving a wallet receive address
*/
export const DeriveAddressBody = {
/**
* Keychains for derivation (public key material only).
* BIP32 multisig: the user/backup/bitgo xpub triple via `pub`.
* TSS/MPC: the `commonKeychain`.
*/
keychains: t.array(DeriveAddressKeychainCodec),
/** Derivation index for the address (caller-supplied; the endpoint is stateless). Non-negative integer. */
index: NonNegativeInteger,
/** Derivation chain code: UTXO script-type / external(0) vs internal(1) selector. Non-negative integer. */
chain: optional(NonNegativeInteger),
/** Address format override (e.g. 'p2sh', 'p2wsh' for UTXO; 'cashaddr' / 'base58') */
format: optional(CreateAddressFormat),
/** Wallet version, to disambiguate derivation strategy (e.g. EVM forwarder vs MPC) */
walletVersion: optional(t.number),
/**
* Seed from the user keychain's derivedFromParentWithSeed field (SMC TSS wallets);
* makes the derivation path `{prefix}/{index}` instead of `m/{index}`.
*/
derivedFromParentWithSeed: optional(t.string),
} as const;

/**
* Response for locally deriving a wallet address
*/
export const DeriveAddressResponse = {
/** The derived address and related derivation info */
200: t.intersection([
t.type({
/** The derived address */
address: t.string,
/** The derivation index used */
index: t.number,
}),
t.partial({
/** The derivation chain code used */
chain: t.number,
/** Coin-specific address data (e.g. redeemScript/witnessScript for UTXO) */
coinSpecific: t.UnknownRecord,
/** The HD derivation path actually used */
derivationPath: t.string,
}),
]),
/** Invalid request parameters or derivation failed */
400: BitgoExpressError,
} as const;

/**
* Locally derive and return a wallet receive address from a derivation path.
*
* Unlike `iswalletaddress` (which checks a candidate address), this *produces* the address
* offline from public key material only — the xpub triple for BIP32 multisig coins, or the
* commonKeychain for TSS/MPC coins. No private keys, no wallet lookup, and no network access:
* the handler operates purely on the request body and can run in an air-gapped Express.
*
* Pairs with `iswalletaddress` for a derive→verify round-trip: derive the address here, then
* verify it against the same keychains to independently confirm correctness.
*
* @operationId express.v2.address.derive
* @tag Express
*/
export const PostDeriveAddress = httpRoute({
path: '/api/v2/{coin}/address/derive',
method: 'POST',
request: httpRequest({
params: DeriveAddressParams,
body: DeriveAddressBody,
}),
response: DeriveAddressResponse,
});
Loading
Loading