PHP WebShell
Текущая директория: /opt/BitGoJS/modules/utxo-lib/test/bitgo/wallet
Просмотр файла: WalletUnspent.ts
import * as assert from 'assert';
import { Transaction, networks } from '../../../src';
import {
isWalletUnspent,
formatOutputId,
getOutputIdForInput,
parseOutputId,
TxOutPoint,
Unspent,
createTransactionBuilderForNetwork,
getInternalChainCode,
getExternalChainCode,
addToTransactionBuilder,
signInputWithUnspent,
WalletUnspentSigner,
outputScripts,
unspentSum,
getWalletAddress,
verifySignatureWithUnspent,
toTNumber,
UtxoTransaction,
createPsbtForNetwork,
createPsbtFromTransaction,
addWalletUnspentToPsbt,
addWalletOutputToPsbt,
toPrevOutput,
KeyName,
signInputP2shP2pk,
} from '../../../src/bitgo';
import { getDefaultWalletKeys } from '../../../src/testutil';
import { defaultTestOutputAmount } from '../../transaction_util';
import {
mockWalletUnspent,
isReplayProtectionUnspent,
mockReplayProtectionUnspent,
replayProtectionKeyPair,
} from '../../../src/testutil/mock';
const CHANGE_INDEX = 100;
const FEE = BigInt(100);
type InputType = outputScripts.ScriptType2Of3 | 'p2shP2pk';
function getScriptTypes2Of3() {
// FIXME(BG-66941): p2trMusig2 signing does not work in this test suite yet
// because the test suite is written with TransactionBuilder
return outputScripts.scriptTypes2Of3.filter((scriptType) => scriptType !== 'p2trMusig2');
}
describe('WalletUnspent', function () {
const network = networks.bitcoin;
const walletKeys = getDefaultWalletKeys();
const hash = Buffer.alloc(32).fill(0xff);
hash[0] = 0; // show endianness
const input = { hash, index: 0 };
const expectedOutPoint: TxOutPoint = {
txid: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00',
vout: 0,
};
it('parses and formats txid', function () {
assert.deepStrictEqual(getOutputIdForInput(input), expectedOutPoint);
assert.deepStrictEqual(
formatOutputId(expectedOutPoint),
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00:0'
);
assert.deepStrictEqual(parseOutputId(formatOutputId(expectedOutPoint)), expectedOutPoint);
});
it('identifies wallet unspents', function () {
const unspent: Unspent = {
id: formatOutputId(expectedOutPoint),
address: getWalletAddress(walletKeys, 0, 0, network),
value: 1e8,
};
assert.strictEqual(isWalletUnspent(unspent), false);
assert.strictEqual(isWalletUnspent({ ...unspent, chain: 0, index: 0 } as Unspent), true);
});
function constructAndSignTransactionUsingPsbt(
unspents: (Unspent<bigint> & { prevTx?: Buffer })[],
signer: KeyName,
cosigner: KeyName,
outputType: outputScripts.ScriptType2Of3
): Transaction<bigint> {
const psbt = createPsbtForNetwork({ network });
const total = BigInt(unspentSum<bigint>(unspents, 'bigint'));
addWalletOutputToPsbt(psbt, walletKeys, getInternalChainCode(outputType), CHANGE_INDEX, total - FEE);
unspents.forEach((u) => {
if (isWalletUnspent(u)) {
addWalletUnspentToPsbt(psbt, u, walletKeys, signer, cosigner);
} else {
throw new Error(`invalid unspent`);
}
});
// TODO: Test rederiving scripts from PSBT and keys only
psbt.signAllInputsHD(walletKeys[signer]);
psbt.signAllInputsHD(walletKeys[cosigner]);
assert(psbt.validateSignaturesOfAllInputs());
psbt.finalizeAllInputs();
// extract transaction has a return type of Transaction instead of UtxoTransaction
const tx = psbt.extractTransaction() as UtxoTransaction<bigint>;
const psbt2 = createPsbtFromTransaction(
tx,
unspents.map((u) => ({ ...toPrevOutput<bigint>(u, network), prevTx: u.prevTx }))
);
assert(psbt2.validateSignaturesOfAllInputs());
return tx;
}
function constructAndSignTransactionUsingTransactionBuilder<TNumber extends number | bigint>(
unspents: Unspent<TNumber>[],
signer: string,
cosigner: string,
amountType: 'number' | 'bigint' = 'number',
outputType: outputScripts.ScriptType2Of3
): UtxoTransaction<TNumber> {
const txb = createTransactionBuilderForNetwork<TNumber>(network);
const total = BigInt(unspentSum<TNumber>(unspents, amountType));
// Kinda weird, treating entire value as change, but tests the relevant paths
txb.addOutput(
getWalletAddress(walletKeys, getInternalChainCode(outputType), CHANGE_INDEX, network),
toTNumber<TNumber>(total - FEE, amountType)
);
unspents.forEach((u) => {
addToTransactionBuilder(txb, u);
});
unspents.forEach((u, i) => {
if (isReplayProtectionUnspent(u, network)) {
signInputP2shP2pk(txb, i, replayProtectionKeyPair);
}
});
[
WalletUnspentSigner.from(walletKeys, walletKeys[signer], walletKeys[cosigner]),
WalletUnspentSigner.from(walletKeys, walletKeys[cosigner], walletKeys[signer]),
].forEach((walletSigner, nSignature) => {
unspents.forEach((u, i) => {
if (isWalletUnspent(u)) {
signInputWithUnspent(txb, i, u, walletSigner);
} else if (isReplayProtectionUnspent(u, network)) {
return;
} else {
throw new Error(`unexpected unspent ${u.id}`);
}
});
const tx = nSignature === 0 ? txb.buildIncomplete() : txb.build();
// Verify each signature for the unspent
unspents.forEach((u, i) => {
if (isReplayProtectionUnspent(u, network)) {
// signature verification not implemented for replay protection unspents
return;
}
assert.deepStrictEqual(
verifySignatureWithUnspent(tx, i, unspents, walletKeys),
walletKeys.triple.map((k) => k === walletKeys[signer] || (nSignature === 1 && k === walletKeys[cosigner]))
);
});
});
return txb.build();
}
function validateLockTimeAndSequence<TNumber extends number | bigint>(
transaction: UtxoTransaction<TNumber> | Transaction<bigint>
) {
// locktime should default to 0 and sequence to 0xffffffff for all inputs
assert.deepStrictEqual(transaction.locktime, 0);
const inputs = transaction.ins;
for (const input of inputs) {
assert.deepStrictEqual(input.sequence, 0xffffffff);
}
}
function runTestSignUnspents<TNumber extends number | bigint>({
inputScriptTypes,
outputScriptType,
signer,
cosigner,
amountType,
testOutputAmount,
}: {
inputScriptTypes: InputType[];
outputScriptType: outputScripts.ScriptType2Of3;
signer: KeyName;
cosigner: KeyName;
amountType: 'number' | 'bigint';
testOutputAmount: TNumber;
}) {
it(`can be signed [inputs=${inputScriptTypes} signer=${signer} cosigner=${cosigner} amountType=${amountType}]`, function () {
const unspents = inputScriptTypes.map((t, i): Unspent<TNumber> => {
if (outputScripts.isScriptType2Of3(t)) {
return mockWalletUnspent(network, testOutputAmount, {
keys: walletKeys,
chain: getExternalChainCode(t),
vout: i,
});
}
if (t === 'p2shP2pk') {
return mockReplayProtectionUnspent(network, toTNumber(1_000, amountType));
}
throw new Error(`invalid input type ${t}`);
});
const txbTransaction = constructAndSignTransactionUsingTransactionBuilder(
unspents,
signer,
cosigner,
amountType,
outputScriptType
);
validateLockTimeAndSequence(txbTransaction);
if (amountType === 'bigint') {
if (inputScriptTypes.includes('p2shP2pk')) {
// FIMXE(BG-47824): add p2shP2pk support for Psbt
return;
}
const psbtTransaction = constructAndSignTransactionUsingPsbt(
unspents as Unspent<bigint>[],
signer,
cosigner,
outputScriptType
);
assert.deepStrictEqual(txbTransaction.toBuffer(), psbtTransaction.toBuffer());
validateLockTimeAndSequence(psbtTransaction);
}
});
}
function getInputScripts(): InputType[][] {
return getScriptTypes2Of3().flatMap((t) => [
[t, t],
[t, t, 'p2shP2pk'],
]);
}
function getSignerPairs(): [signer: KeyName, cosigner: KeyName][] {
const keyNames: KeyName[] = ['user', 'backup', 'bitgo'];
return keyNames.flatMap((signer) =>
keyNames.flatMap((cosigner): [KeyName, KeyName][] => (signer === cosigner ? [] : [[signer, cosigner]]))
);
}
getInputScripts().forEach((inputScriptTypes) => {
getSignerPairs().forEach(([signer, cosigner]) => {
runTestSignUnspents({
inputScriptTypes,
outputScriptType: 'p2sh',
signer,
cosigner,
amountType: 'number',
testOutputAmount: defaultTestOutputAmount,
});
runTestSignUnspents<bigint>({
inputScriptTypes,
outputScriptType: 'p2sh',
signer,
cosigner,
amountType: 'bigint',
testOutputAmount: BigInt('10000000000000000'),
});
});
});
});
Выполнить команду
Для локальной разработки. Не используйте в интернете!