Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ac15c77
feat(sdk-coin-eth): add registerWithCoinMap for dynamic token registr…
manas-at-bitgo Apr 21, 2026
428b258
feat: add new tokens for CSHLD-701
asset-metadata-bot[bot] Apr 28, 2026
5ae22c0
feat(abstract-utxo): bump wasm-utxo to 4.8.0
OttoAllmendinger Apr 28, 2026
e7dfa8e
feat: add round domain separator to adata
zhongxishen Apr 17, 2026
6c87b40
Merge remote-tracking branch 'origin/HSM-1513'
zhongxishen Apr 28, 2026
5f0d1ed
fix(abstract-utxo): guard address in checkRecipient for OP_RETURN out…
mohammadalfaiyazbitgo Apr 28, 2026
c474524
fix(sdk-lib-mpc): use @bitgo/wasm-mps/web in browser for EdDSA DKG
Marzooqa Apr 28, 2026
3ced71b
Merge pull request #8589 from BitGo/feat/CSHLD-24-register-token
manas-at-bitgo Apr 28, 2026
c0dbca6
feat(root): add recovery-mode to npmjs-release workflow
zahin-mohammad Apr 28, 2026
447d400
Merge pull request #8649 from BitGo/worktree-wal-826-fix-check-recipient
mohammadalfaiyazbitgo Apr 29, 2026
911965e
Merge pull request #8650 from BitGo/zahinmohammad/WCN-308-recovery-mode
lokesh-bitgo Apr 29, 2026
9742f94
Merge pull request #8643 from BitGo/wci-251-eddsa-mps-browser-wasm-fix
Marzooqa Apr 29, 2026
5c30948
feat(statics): add OFC coins for kavacosmos and dydxcosmos Go Accounts
abhijeet8986 Apr 29, 2026
4cd2194
Merge pull request #8647 from BitGo/BTC-0.bump-wasm-utxo-4.8.0
OttoAllmendinger Apr 29, 2026
8fcf1da
feat(sdk-lib-mpc): add EdDSA DSG MPS class
vibhavgo Apr 22, 2026
661a078
Merge pull request #8651 from BitGo/CHALO-355
abhijeet8986 Apr 29, 2026
d1e2a12
test(abstract-utxo): move BIP-322 Proof tests
OttoAllmendinger Apr 28, 2026
b6b6166
fix(abstract-utxo): enhance BIP-322 with signature verification
OttoAllmendinger Apr 28, 2026
e124588
Merge pull request #8645 from BitGo/ams-bot-tokens
manas-at-bitgo Apr 29, 2026
9d90a6d
ci(root): upgrade aws-actions/configure-aws-credentials to v6
Apr 19, 2026
98f844b
Merge pull request #8653 from BitGo/BTC-0.bip322-fixes
OttoAllmendinger Apr 29, 2026
b57c529
Merge pull request #8562 from BitGo/dx-496-upgrade-aws-credentials-v6
ericcrosson-bitgo Apr 29, 2026
b37a6a1
Merge pull request #8599 from BitGo/feat/eddsa-dsg-class
vibhavgo Apr 29, 2026
8542b17
Revert "feat: implement BitGo signing in SDK"
alextse-bg Apr 29, 2026
df2d71a
Merge pull request #8655 from BitGo/revert-8624-WCN-217
alextse-bg Apr 29, 2026
ae0ed9c
chore(root): merge master into rel/latest
zahin-mohammad Apr 29, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/claude-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v6

- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v5
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::199765120567:role/${{ github.event.repository.name }}-iam-protected
aws-region: us-west-2
Expand Down
120 changes: 117 additions & 3 deletions .github/workflows/npmjs-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,33 @@ on:
type: boolean
required: false
default: false
recovery-mode:
description: |
Recover from a partial-publish failure. Skips version bumping,
master→rel/latest merge, and GPG signing. Runs `lerna publish
from-package` against rel/latest, publishing only versions
missing from npm. Release notes, GitHub release, and Express
Docker publish still run.

IMPORTANT: from-package publishes whatever versions are in the
rel/latest package.json files at trigger time. Verify rel/latest
HEAD matches the failed release before triggering — the workflow
logs the resolved SHA and the planned publish list.
type: boolean
required: false
default: false

permissions:
contents: write
id-token: write
pull-requests: read

# Prevent overlapping releases. workflow_dispatch runs are serialized;
# a normal release and a recovery run cannot race against rel/latest.
concurrency:
group: npmjs-release
cancel-in-progress: false

env:
NX_NO_CLOUD: true
NX_SKIP_NX_CACHE: true
Expand All @@ -24,6 +45,7 @@ env:
jobs:
get-release-context:
name: Get release context
if: ${{ !inputs.recovery-mode }}
runs-on: ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }}
timeout-minutes: 10
outputs:
Expand Down Expand Up @@ -109,23 +131,79 @@ jobs:

echo "" >> "$GITHUB_STEP_SUMMARY"

get-recovery-context:
name: Get recovery context
if: inputs.recovery-mode
runs-on: ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }}
timeout-minutes: 10
outputs:
# Pinned SHA. release-bitgojs checks out this exact commit, not the
# rel/latest branch tip, so the publish cannot drift from what the
# env reviewer approved.
sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Checkout rel/latest
uses: actions/checkout@v6
with:
ref: rel/latest
fetch-depth: 1

- name: Resolve SHA and show recovery target
id: resolve
run: |
# Pin the SHA at preview time and surface it (plus the planned
# publish list) BEFORE the env-gated publish job runs, so
# reviewers approving the `npmjs-release` environment can
# sanity-check what will be published.
sha="$(git rev-parse HEAD)"
if [ -z "$sha" ]; then
echo "::error::Failed to resolve rel/latest SHA. Refusing to proceed."
exit 1
fi
echo "sha=$sha" >> "$GITHUB_OUTPUT"
{
echo "## Recovery target"
echo ""
echo "Branch: \`rel/latest\`"
echo "Resolved SHA: \`$sha\`"
echo "Subject: $(git log -1 --pretty=format:'%s')"
echo ""
echo "### Versions in rel/latest package.jsons"
echo ""
echo '```'
for f in modules/*/package.json; do
jq -r '"\(.name)@\(.version)\(if .private then " (private)" else "" end)"' "$f"
done | sort
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

release-bitgojs:
name: Release BitGoJS
needs:
- get-release-context
- get-recovery-context
if: ${{ always() && needs.get-release-context.result != 'failure' && needs.get-recovery-context.result != 'failure' }}
runs-on: ${{ vars.BASE_RUNNER_TYPE || 'ubuntu-latest' }}
timeout-minutes: 60
environment: npmjs-release
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ needs.get-release-context.outputs.current-master-sha }}
# Recovery mode pins to the SHA resolved by get-recovery-context so
# the publish cannot drift from the commit the env reviewer approved
# (rel/latest could otherwise advance during the approval wait).
# Normal mode uses the master SHA captured by get-release-context.
ref: ${{ inputs.recovery-mode && needs.get-recovery-context.outputs.sha || needs.get-release-context.outputs.current-master-sha }}
token: ${{ secrets.BITGOBOT_PAT_TOKEN || github.token }}
fetch-depth: 0
# version-bump-summary uses `git tag --points-at HEAD`. In recovery
# mode the bump tags were created by a prior failed run and live
# only on origin, so we must fetch them.
fetch-tags: true

- name: Configure GPG
if: inputs.dry-run == false
if: ${{ inputs.dry-run == false && !inputs.recovery-mode }}
run: |
echo "${{ secrets.BITGOBOT_GPG_PRIVATE_KEY }}" | gpg --batch --import
git config --global user.signingkey 67A9A0B77F0BD445E45CC8B719828A304678A92F
Expand All @@ -143,11 +221,13 @@ jobs:
node-version-file: ".nvmrc"

- name: Switch to rel/latest branch
if: ${{ !inputs.recovery-mode }}
run: |
git checkout rel/latest
git pull origin rel/latest

- name: Merge master into rel/latest
if: ${{ !inputs.recovery-mode }}
run: |
echo "Merging master commit ${{ needs.get-release-context.outputs.current-master-sha }} into rel/latest"
git merge ${{ needs.get-release-context.outputs.current-master-sha }} --no-edit
Expand All @@ -171,12 +251,46 @@ jobs:
uses: ./.github/actions/verify-npm-packages

- name: Publish new version
if: inputs.dry-run == false
if: ${{ inputs.dry-run == false && !inputs.recovery-mode }}
run: |
yarn lerna publish --sign-git-tag --sign-git-commit --include-merged-tags --conventional-commits --conventional-graduate --yes
env:
NPM_CONFIG_PROVENANCE: true

- name: Publish missing versions (recovery)
if: ${{ inputs.dry-run == false && inputs.recovery-mode }}
run: |
# `from-package` reads each package.json's `version`, queries npm,
# and publishes only versions missing from the registry. No bump,
# no tag, no git push.
yarn lerna publish from-package --yes
env:
NPM_CONFIG_PROVENANCE: true

- name: Verify recovery published the missing versions
if: ${{ inputs.dry-run == false && inputs.recovery-mode }}
run: |
# Walk every non-private package and confirm the version on
# rel/latest is now reachable on the npm registry. Catches the
# case where lerna reports success but a package didn't land
# (e.g., another transient registry error).
missing=()
for f in modules/*/package.json; do
if [ "$(jq -r '.private // false' "$f")" = "true" ]; then continue; fi
name=$(jq -r '.name' "$f")
version=$(jq -r '.version' "$f")
code=$(curl -sL -o /dev/null -w "%{http_code}" -- "https://registry.npmjs.org/${name}/${version}")
if [ "$code" != "200" ]; then
missing+=("${name}@${version} (HTTP ${code})")
fi
done
if [ "${#missing[@]}" -ne 0 ]; then
echo "::error::Recovery left versions still missing from npm:"
printf ' - %s\n' "${missing[@]}"
exit 1
fi
echo "✅ All public package versions on rel/latest are present on npm."

- name: Generate version bump summary
id: version-bump-summary
if: inputs.dry-run == false
Expand Down
2 changes: 1 addition & 1 deletion modules/abstract-utxo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@bitgo/utxo-core": "^1.36.1",
"@bitgo/utxo-lib": "^11.22.0",
"@bitgo/utxo-ord": "^1.29.1",
"@bitgo/wasm-utxo": "^4.7.0",
"@bitgo/wasm-utxo": "^4.8.0",
"@types/lodash": "^4.14.121",
"@types/superagent": "4.1.15",
"bignumber.js": "^9.0.2",
Expand Down
6 changes: 3 additions & 3 deletions modules/abstract-utxo/src/abstractUtxoCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,10 +572,10 @@ export abstract class AbstractUtxoCoin
return (chainhead as any).height;
}

checkRecipient(recipient: { address: string; amount: number | string }): void {
checkRecipient(recipient: { address?: string; amount: number | string }): void {
assertValidTransactionRecipient(recipient);
if (!isScriptRecipient(recipient.address)) {
super.checkRecipient(recipient);
if (recipient.address && !isScriptRecipient(recipient.address)) {
super.checkRecipient({ address: recipient.address, amount: recipient.amount });
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
import { fixedScriptWallet, bip322 } from '@bitgo/wasm-utxo';
import { Triple } from '@bitgo/sdk-core';

import type { FixedScriptWalletOutput, Output, BitGoPsbt } from '../types';
import type { Bip322Message } from '../../abstractUtxoCoin';

import type { TransactionExplanationWasm } from './explainTransaction';

Expand Down Expand Up @@ -49,6 +50,8 @@ interface ExplainPsbtWasmParams {
export interface ExplainedInput<TAmount = bigint> {
address: string;
value: TAmount;
scriptId: fixedScriptWallet.ScriptId | null;
signedBy: { [key: string]: boolean };
}

export interface TransactionExplanationBigInt {
Expand All @@ -64,9 +67,28 @@ export interface TransactionExplanationBigInt {
fee: bigint;
}

function getSignedByForInput(
psbt: BitGoPsbt,
inputIndex: number,
walletXpubs: fixedScriptWallet.RootWalletKeys,
replayProtectionPublicKeys: Buffer[],
scriptId: fixedScriptWallet.ScriptId | null
): { [key: string]: boolean } {
if (scriptId !== null) {
return {
user: psbt.verifySignature(inputIndex, walletXpubs.userKey()),
backup: psbt.verifySignature(inputIndex, walletXpubs.backupKey()),
bitgo: psbt.verifySignature(inputIndex, walletXpubs.bitgoKey()),
};
}
return Object.fromEntries(
replayProtectionPublicKeys.map((key, j) => [`replayProtection${j}`, psbt.verifySignature(inputIndex, key)])
);
}

export function explainPsbtWasmBigInt(
psbt: BitGoPsbt,
walletXpubs: Triple<string> | fixedScriptWallet.RootWalletKeys,
walletXpubs: fixedScriptWallet.RootWalletKeys,
params: ExplainPsbtWasmParams
): TransactionExplanationBigInt {
const parsed = psbt.parseTransactionWithWalletKeys(walletXpubs, { replayProtection: params.replayProtection });
Expand All @@ -92,7 +114,12 @@ export function explainPsbtWasmBigInt(
}
});

const inputs = parsed.inputs.map((input) => ({ address: input.address, value: input.value }));
const inputs = parsed.inputs.map((input, i) => ({
address: input.address,
value: input.value,
scriptId: input.scriptId,
signedBy: getSignedByForInput(psbt, i, walletXpubs, params.replayProtection.publicKeys, input.scriptId),
}));
const inputAmount = inputs.reduce((sum, input) => sum + input.value, 0n);
const outputAmount = outputs.reduce((sum, output) => sum + output.amount, 0n);
const changeAmount = changeOutputs.reduce((sum, output) => sum + output.amount, 0n);
Expand Down Expand Up @@ -120,15 +147,32 @@ function stringifyChangeOutput(output: FixedScriptWalletOutput<bigint>): FixedSc
return { ...output, amount: output.amount.toString() };
}

function extractBip322Messages(psbt: BitGoPsbt, inputs: ExplainedInput[]): { messages: Bip322Message[] | undefined } {
const messages: Bip322Message[] = inputs.flatMap((input, i) => {
const message = bip322.getBip322Message(psbt, i);
return message ? [{ message, address: input.address }] : [];
});

if (messages.length === 0) {
return { messages: undefined };
}

return { messages };
}

export function explainPsbtWasm(
psbt: BitGoPsbt,
walletXpubs: Triple<string> | fixedScriptWallet.RootWalletKeys,
params: ExplainPsbtWasmParams
): TransactionExplanationWasm {
const result = explainPsbtWasmBigInt(psbt, walletXpubs, params);
const result = explainPsbtWasmBigInt(psbt, fixedScriptWallet.RootWalletKeys.from(walletXpubs), params);
const inputs = result.inputs.map((i) => ({ address: i.address, value: i.value.toString(), signedBy: i.signedBy }));

const { messages } = extractBip322Messages(psbt, result.inputs);

return {
id: result.id,
inputs: result.inputs.map((i) => ({ address: i.address, value: i.value.toString() })),
inputs,
inputAmount: result.inputAmount.toString(),
outputAmount: result.outputAmount.toString(),
changeAmount: result.changeAmount.toString(),
Expand All @@ -137,6 +181,7 @@ export function explainPsbtWasm(
changeOutputs: result.changeOutputs.map(stringifyChangeOutput),
customChangeOutputs: result.customChangeOutputs.map(stringifyChangeOutput),
fee: result.fee.toString(),
messages,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ interface TransactionExplanationWithSignatures<TFee = string, TChangeOutput exte

/** For our wasm backend, we do not return the deprecated fields. We set TFee to string for backwards compatibility. */
export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation<string, FixedScriptWalletOutput> & {
inputs: Array<{ address: string; value: string }>;
inputs: Array<{ address: string; value: string; signedBy: { [key: string]: boolean } }>;
inputAmount: string;
};

Expand Down
Loading
Loading