Skip to content

Commit

Permalink
Merge pull request #17 from bob-collective/refactor/op-return
Browse files Browse the repository at this point in the history
refactor: allow op-return
  • Loading branch information
gregdhill authored Aug 22, 2024
2 parents 4c86703 + 474315e commit e34ab61
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 33 deletions.
2 changes: 1 addition & 1 deletion packages/snap/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gobob/bob-snap",
"version": "2.2.1",
"version": "2.2.2",
"description": "BOB: Metamask snap to manage your BTC, ordinals, and more",
"contributors": [
{
Expand Down
4 changes: 2 additions & 2 deletions packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"version": "2.2.1",
"version": "2.2.2",
"description": "BOB: Metamask snap to manage your Bitcoin",
"proposedName": "BOB",
"repository": {
"type": "git",
"url": "https://github.com/bob-collective/bob-snap"
},
"source": {
"shasum": "HHxMGzE7qqYdjyYNB6L+2droaS0ElKFS1CIyDl4pIo0=",
"shasum": "tUeqZ/Wk3Ot1VfrXoGa8QVw1dtLO3Sm1kQ3/3+UVjIk=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
35 changes: 23 additions & 12 deletions packages/snap/src/bitcoin/PsbtHelper.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
import { address, Network, Psbt, Transaction } from 'bitcoinjs-lib';
import { address, Network, opcodes, Psbt, script, Transaction } from 'bitcoinjs-lib';
import { getNetwork } from './getNetwork';
import { BitcoinNetwork } from '../interface';

export class PsbtHelper {
private tx: Psbt;
private psbt: Psbt;
private network: Network;

constructor(psbt: Psbt, network: BitcoinNetwork) {
this.network = getNetwork(network);
this.tx = psbt;
this.psbt = psbt;
}

get inputAmount() {
return this.tx.data.inputs.reduce((total, input, index) => {
const vout = this.tx.txInputs[index].index;
return this.psbt.data.inputs.reduce((total, input, index) => {
const vout = this.psbt.txInputs[index].index;
const prevTx = Transaction.fromHex(input.nonWitnessUtxo.toString('hex'));
return total + prevTx.outs[vout].value;
}, 0);
}

get sendAmount() {
return this.tx.txOutputs
return this.psbt.txOutputs
.filter(output => !this.changeAddresses.includes(output.address))
.reduce((amount, output) => amount + output.value, 0);
}

get fee() {
const outputAmount = this.tx.txOutputs.reduce((amount, output) => amount + output.value, 0);
const outputAmount = this.psbt.txOutputs.reduce((amount, output) => amount + output.value, 0);
return this.inputAmount - outputAmount;
}

get fromAddresses() {
return this.tx.data.inputs.map((input, index) => {
return this.psbt.data.inputs.map((input, index) => {
const prevOuts = Transaction.fromHex(input.nonWitnessUtxo.toString('hex')).outs
const vout = this.tx.txInputs[index].index;
const vout = this.psbt.txInputs[index].index;
return address.fromOutputScript(prevOuts[vout].script, this.network)
})
}

get toAddresses() {
return this.tx.txOutputs.map(output => output.address).filter(address => !this.changeAddresses.includes(address));
return this.psbt.txOutputs.map(output => {
if (output.address == null) {
const scriptPubKey = script.decompile(output.script);
if (scriptPubKey.length == 2 && scriptPubKey[0] == opcodes.OP_RETURN && Buffer.isBuffer(scriptPubKey[1])) {
return `OP_RETURN 0x${scriptPubKey[1].toString("hex")}`;
} else {
return "Unknown";
}
} else {
return output.address;
}
}).filter(address => !this.changeAddresses.includes(address));
}

get changeAddresses() {
return this.tx.data.outputs
.map((output, index) => output.bip32Derivation ? this.tx.txOutputs[index].address : undefined)
return this.psbt.data.outputs
.map((output, index) => output.bip32Derivation ? this.psbt.txOutputs[index].address : undefined)
.filter(address => !!address)
}
}
39 changes: 24 additions & 15 deletions packages/snap/src/bitcoin/PsbtValidator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Psbt } from 'bitcoinjs-lib';
import { Psbt, opcodes, script } from 'bitcoinjs-lib';
import { AccountSigner } from './index';
import { BitcoinNetwork } from '../interface';
import { PsbtHelper } from '../bitcoin/PsbtHelper';
Expand All @@ -20,31 +20,31 @@ function checkForInput<PsbtInput>(inputs: PsbtInput[], inputIndex: number): Psbt

export class PsbtValidator {
static FEE_THRESHOLD = 10000000;
private readonly tx: Psbt;
private readonly psbt: Psbt;
private readonly snapNetwork: BitcoinNetwork;
private psbtHelper: PsbtHelper;
private error: SnapError | null = null;

constructor(psbt: Psbt, network: BitcoinNetwork) {
this.tx = psbt;
this.psbt = psbt;
this.snapNetwork = network;
this.psbtHelper = new PsbtHelper(this.tx, network);
this.psbtHelper = new PsbtHelper(this.psbt, network);
}

get coinType() {
return this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAINNET_COIN_TYPE : BITCOIN_TESTNET_COIN_TYPE;
}

allInputsHaveRawTxHex() {
const result = this.tx.data.inputs.every((input, index) => !!input.nonWitnessUtxo);
const result = this.psbt.data.inputs.every((input, index) => !!input.nonWitnessUtxo);
if (!result) {
this.error = SnapError.of(PsbtValidateErrors.InputsDataInsufficient);
}
return result;
}

everyInputMatchesNetwork() {
const result = this.tx.data.inputs.every(input => {
const result = this.psbt.data.inputs.every(input => {
if (isTaprootInput(input)) {
return input.tapBip32Derivation.every(derivation => {
const { coinType } = fromHdPathToObj(derivation.path);
Expand All @@ -65,7 +65,7 @@ export class PsbtValidator {

everyOutputMatchesNetwork() {
const addressPattern = this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAIN_NET_ADDRESS_PATTERN : BITCOIN_TEST_NET_ADDRESS_PATTERN;
const result = this.tx.data.outputs.every((output, index) => {
const result = this.psbt.data.outputs.every((output, index) => {
if (output.tapBip32Derivation) {
return output.tapBip32Derivation.every(derivation => {
const { coinType } = fromHdPathToObj(derivation.path)
Expand All @@ -77,7 +77,16 @@ export class PsbtValidator {
return Number(coinType) === this.coinType
})
} else {
const address = this.tx.txOutputs[index].address;
const scriptPubKey = script.decompile(this.psbt.txOutputs[index].script);
if (scriptPubKey.length == 2 && scriptPubKey[0] == opcodes.OP_RETURN && Buffer.isBuffer(scriptPubKey[1])) {
if (scriptPubKey[1].byteLength > 80) {
// miners will reject anything over 80 bytes
this.error = SnapError.of(PsbtValidateErrors.InvalidOpReturn);
}
// as an exception we allow OP_RETURN outputs
return true;
}
const address = this.psbt.txOutputs[index].address;
return addressPattern.test(address);
}
})
Expand All @@ -89,12 +98,12 @@ export class PsbtValidator {
}

allInputsBelongToCurrentAccount(accountSigner: AccountSigner) {
const result = this.tx.txInputs.every((_, index) => {
const input = checkForInput(this.tx.data.inputs, index);
const result = this.psbt.txInputs.every((_, index) => {
const input = checkForInput(this.psbt.data.inputs, index);
if (isTaprootInput(input)) {
return tapInputHasHDKey(input, accountSigner);
} else {
return this.tx.inputHasHDKey(index, accountSigner);
return this.psbt.inputHasHDKey(index, accountSigner);
}
});
if (!result) {
Expand All @@ -104,11 +113,11 @@ export class PsbtValidator {
}

changeAddressBelongsToCurrentAccount(accountSigner: AccountSigner) {
const result = this.tx.data.outputs.every((output, index) => {
const result = this.psbt.data.outputs.every((output, index) => {
if (output.tapBip32Derivation) {
return tapOutputHasHDKey(output, accountSigner);
} else if (output.bip32Derivation) {
return this.tx.outputHasHDKey(index, accountSigner);
return this.psbt.outputHasHDKey(index, accountSigner);
}
return true;
});
Expand All @@ -127,12 +136,12 @@ export class PsbtValidator {
}

witnessUtxoValueMatchesNoneWitnessOnes() {
const hasWitnessUtxo = this.tx.data.inputs.some((_, index) => this.tx.getInputType(index) === "witnesspubkeyhash");
const hasWitnessUtxo = this.psbt.data.inputs.some((_, index) => this.psbt.getInputType(index) === "witnesspubkeyhash");
if (!hasWitnessUtxo) {
return true;
}

const witnessAmount = this.tx.data.inputs.reduce((total, input, index) => {
const witnessAmount = this.psbt.data.inputs.reduce((total, input, index) => {
return total + input.witnessUtxo.value;
}, 0);
const result = this.psbtHelper.inputAmount === witnessAmount;
Expand Down
31 changes: 30 additions & 1 deletion packages/snap/src/bitcoin/__tests__/PsbtValidator.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import BIP32Factory from 'bip32';
import { networks, Psbt } from 'bitcoinjs-lib';
import { networks, opcodes, Psbt, script } from 'bitcoinjs-lib';
import { PsbtValidator } from '../PsbtValidator';
import { AccountSigner } from '../index';
import { BitcoinNetwork } from '../../interface';
import { psbtFixture } from './fixtures/psbt';
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
import { PsbtHelper } from '../PsbtHelper';

const getAccountSigner = () => {
const testPrivateAccountKey = "tprv8gwYx7tEWpLxdJhEa7R8ofchqzRgme6iiuyJpegZ71XNhnAqeMjT6GV4wm3jqsUjXgXj99GB4kDminso5kxnLa6VXt3WVRzfmhbDSrfbCDv";
Expand Down Expand Up @@ -184,4 +185,32 @@ describe('psbtValidator', () => {

expect(psbtValidator.validate(signer)).toBe(true);
});

it('should return true given a valid psbt with OP_RETURN', function () {
const psbt = Psbt.fromBase64(psbtFixture.base64, { network: networks.testnet })
psbt.addOutput({
script: script.compile([opcodes.OP_RETURN, Buffer.alloc(20, 0)]),
value: 0,
})

const psbtHelper = new PsbtHelper(psbt, BitcoinNetwork.Test);
expect(psbtHelper.toAddresses).toEqual([
'tb1qqkelutyrqmxgzd9nnfws2yk3dl600yvxagfqu7',
'OP_RETURN 0x0000000000000000000000000000000000000000'
]);

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(psbtValidator.validate(signer)).toBe(true);
});

it('should throw error when OP_RETURN is too big', function () {
const psbt = Psbt.fromBase64(psbtFixture.base64, { network: networks.testnet })
psbt.addOutput({
script: script.compile([opcodes.OP_RETURN, Buffer.alloc(81, 0)]),
value: 0,
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => { psbtValidator.validate(signer) }).toThrowError(`Transaction has an invalid OP_RETURN`);
});
});
1 change: 0 additions & 1 deletion packages/snap/src/bitcoin/__tests__/fixtures/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,4 @@ export const psbtFixture = {
],
},
base64: 'cHNidP8BAHECAAAAASpPo+Jtjb8ce88iDgUe9MdBl/N0RXzOyTt6bYRF4KxgAAAAAAD/////AkANAwAAAAAAFgAUBbP+LIMGzIE0s5pdBRLRb/T3kYaazQsAAAAAABYAFDUcL8Uq83TUCsXnr18EDVw08zQ9AAAAAAABAP1UAQIAAAAAAQIbzNzgXMu2XcHbu/VK6Tv2aYkp3WBu0PijNRnjyvh1eQEAAAAA/////wL6QmQPXjZt03YXBX2QbexW+etvDRpeUJE+cz7D5ardAAAAAAD/////Afz3DgAAAAAAFgAUvApnUSw4MVXYWN2ZqWf3hCAWGsoCSDBFAiEA+axvhH4bFn2mSxo6xzybYtrAjdpG0YzlqBam0UNaE3kCIA0lg97qGHi0rKC7hWQXnMSnWbaCII6nGpHErzpoSHNMASEDSB6PkHcBABG+ayUezMfaQN0i6+DO4DwxtF+nbuWWp+ICRzBEAiAYgnrWAOCiD55kZsBSiX/UNMnDsmhcFzKno/6T1nP92gIgPtzzZuB0FjdMrkpzWkDZurYJR7MBcRnszVgqyjX5xEsBIQMR9PpNCfA5TzCasyKhJsp1vevr907O2Kru24aJS/h08QAAAAABAR/89w4AAAAAABYAFLwKZ1EsODFV2Fjdmaln94QgFhrKIgYDSB6PkHcBABG+ayUezMfaQN0i6+DO4DwxtF+nbuWWp+IY+BLROVQAAIABAACAAAAAgAEAAAAKAAAAAAAiAgJgiLaAsqyAi3BUwv3EgxEuXiYfGOFeDgCax2fzYa/sZBj4EtE5VAAAgAEAAIAAAACAAQAAAAsAAAAA',

};
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ export const PsbtValidateErrors = {
AmountNotMatch: {
code: 10007,
message: 'Transaction input amount not match'
},
InvalidOpReturn: {
code: 10008,
message: 'Transaction has an invalid OP_RETURN'
}
}
2 changes: 1 addition & 1 deletion packages/snap/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { SnapError } from './SnapError';
export { PsbtValidateErrors } from './constant/PsbtValidaeErrors';
export { PsbtValidateErrors } from './constant/PsbtValidateErrors';
export { RequestErrors } from './constant/RequestErrors';
export { InvoiceErrors } from './constant/InvoiceErrors';

0 comments on commit e34ab61

Please sign in to comment.