Skip to content

Commit

Permalink
OpDrop Support and recovering public key from the script sig on unwrap.
Browse files Browse the repository at this point in the history
  • Loading branch information
radicleart committed Mar 17, 2023
1 parent a3fd649 commit a40383e
Show file tree
Hide file tree
Showing 19 changed files with 868 additions and 5,930 deletions.
6,417 changes: 601 additions & 5,816 deletions bridge-api/package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions bridge-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "bridge-api",
"version": "1.0.0",
"description": "Enables caching of contract data for the sBTC Bridge app",
"main": "dist/app.js",
"main": "dist/src/app.js",
"type": "module",
"scripts": {
"start": "NODE_ENV=development tsc && NODE_ENV=development node dist/src/app.js",
"start-stag": "NODE_ENV=staging tsc && NODE_ENV=staging node dist/src/app.js",
Expand All @@ -13,17 +14,17 @@
"coverage": "vitest run --coverage",
"predev": "npm run swagger",
"prebuild": "npm run swagger",
"dev": "concurrently \"NODE_ENV=test nodemon --trace-warnings\" \"NODE_ENV=test nodemon -x tsoa spec\"",
"dev": "concurrently \"NODE_ENV=test nodemon \" \"NODE_ENV=test nodemon -x tsoa spec\"",
"swagger": "tsoa spec"
},
"nodemonConfig": {
"watch": [
"src"
],
"ext": "ts",
"exec": "ts-node-esm src/app.ts",
"exec": "ts-node src/app.ts",
"execMap": {
"ts": "ts-node-esm"
"ts": "ts-node"
}
},
"keywords": [],
Expand Down Expand Up @@ -58,6 +59,7 @@
"body-parser": "^1.20.1",
"c32check": "^2.0.0",
"cors": "^2.8.5",
"esm": "^3.2.25",
"express": "^4.18.2",
"jsontokens": "^4.0.1",
"micro-stacks": "^1.1.4",
Expand Down
8 changes: 4 additions & 4 deletions bridge-api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { dumpConfig, sbtcContractId, host, port } from './lib/config';
import { swagger } from './lib/swagger'
import { dumpConfig, sbtcContractId, host, port } from './lib/config.js';
import { swagger } from './lib/swagger.js'
import express, { Application } from "express";
import morgan from "morgan";
import Router from "./routes";
import Router from "./routes/index.js";
import { serve, setup } from 'swagger-ui-express';
import { sbtcEventJob } from './controllers/JobScheduler';
import { sbtcEventJob } from './controllers/JobScheduler.js';
import cors from "cors";

const app = express();
Expand Down
12 changes: 6 additions & 6 deletions bridge-api/src/controllers/BitcoinRPCController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Get, Route } from "tsoa";
import { fetchRawTx } from '../lib/bitcoin/rpc_transaction';
import { getAddressInfo, estimateSmartFee, loadWallet, unloadWallet, listWallets } from "../lib/bitcoin/rpc_wallet";
import { getBlockChainInfo, getBlockCount } from "../lib/bitcoin/rpc_blockchain";
import { fetchUTXOs } from "../lib/bitcoin/mempool_api";
import { fetchCurrentFeeRates as fetchCurrentFeeRatesCypher } from "../lib/bitcoin/blockcypher_api";
import { btcNode, btcRpcUser, btcRpcPwd, walletPath } from '../lib/config';
import { fetchRawTx } from '../lib/bitcoin/rpc_transaction.js';
import { getAddressInfo, estimateSmartFee, loadWallet, unloadWallet, listWallets } from "../lib/bitcoin/rpc_wallet.js";
import { getBlockChainInfo, getBlockCount } from "../lib/bitcoin/rpc_blockchain.js";
import { fetchUTXOs } from "../lib/bitcoin/mempool_api.js";
import { fetchCurrentFeeRates as fetchCurrentFeeRatesCypher } from "../lib/bitcoin/blockcypher_api.js";
import { btcNode, btcRpcUser, btcRpcPwd, walletPath } from '../lib/config.js';

export interface FeeEstimateResponse {
feeInfo: {
Expand Down
2 changes: 1 addition & 1 deletion bridge-api/src/controllers/ConfigController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Get, Route } from "tsoa";
import { dumpConfig } from '../lib/config';
import { dumpConfig } from '../lib/config.js';

export interface IStringToStringDictionary { [key: string]: string|undefined; }
@Route("/bridge-api/v1/config")
Expand Down
2 changes: 1 addition & 1 deletion bridge-api/src/controllers/JobScheduler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cron from 'node-cron';
import { saveAllSbtcEvents } from '../lib/sbtc_rpc';
import { saveAllSbtcEvents } from '../lib/sbtc_rpc.js';


export const sbtcEventJob = cron.schedule('*/5 * * * *', (fireDate) => {
Expand Down
2 changes: 1 addition & 1 deletion bridge-api/src/controllers/StacksRPCController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Get, Route } from "tsoa";
import { indexSbtcEvent, findSbtcEvents, fetchNoArgsReadOnly, saveSbtcEvents, saveAllSbtcEvents, fetchUserSbtcBalance, fetchSbtcWalletAddress } from '../lib/sbtc_rpc';
import { indexSbtcEvent, findSbtcEvents, fetchNoArgsReadOnly, saveSbtcEvents, saveAllSbtcEvents, fetchUserSbtcBalance, fetchSbtcWalletAddress } from '../lib/sbtc_rpc.js';

export interface BalanceI {
balance: number;
Expand Down
2 changes: 1 addition & 1 deletion bridge-api/src/lib/bitcoin/blockcypher_api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { blockCypherUrl } from '../config';
import { blockCypherUrl } from '../config.js';
import fetch from 'node-fetch';

export async function fetchCurrentFeeRates() {
Expand Down
4 changes: 2 additions & 2 deletions bridge-api/src/lib/bitcoin/mempool_api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mempoolUrl } from '../config';
import { hexToAscii, decodeStacksAddress } from "../stacks_helper";
import { mempoolUrl } from '../config.js';
import { hexToAscii, decodeStacksAddress } from "../stacks_helper.js";
import fetch from 'node-fetch';

/**
Expand Down
4 changes: 2 additions & 2 deletions bridge-api/src/lib/bitcoin/rpc_blockchain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getAddressInfo } from './rpc_wallet'
import { getAddressInfo } from './rpc_wallet.js'
import fetch from 'node-fetch';
import { BASE_URL, OPTIONS, handleError } from '../../controllers/BitcoinRPCController'
import { BASE_URL, OPTIONS, handleError } from '../../controllers/BitcoinRPCController.js'

export async function startScantxoutset(address:string) {
const addressInfo:any = await getAddressInfo(address);
Expand Down
179 changes: 114 additions & 65 deletions bridge-api/src/lib/bitcoin/rpc_transaction.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import fetch from 'node-fetch';
import { BASE_URL, OPTIONS } from '../../controllers/BitcoinRPCController'
import { hexToAscii, decodeStacksAddress } from "../stacks_helper";
import { BASE_URL, OPTIONS } from '../../controllers/BitcoinRPCController.js'
import { hexToAscii, decodeStacksAddress } from "../stacks_helper.js";
import util from 'util'
//import btc from '@scure/btc-signer';
import { getBlock } from './rpc_blockchain';
import { bitcoinToSats } from '../utils';
import { network } from '../config';
import { getStacksAddressFromSignature } from '../structured-data'
//import * as btc from '@scure/btc-signer';

//const btc = async ():Promise<any> => {
// await import('micro-btc-signer');
//};
//console.log('btc0 ', util.inspect(btc, false, null, true /* enable colors */));

//const btc = await import('micro-btc-signer');
//import btc from '@scure/btc-signer';
//const btc = (...args: any[]) => import('@scure/btc-signer').then(({default: btc}) => (...args: any));
//const { NETWORK } = await import('micro-btc-signer');
import { getBlock } from './rpc_blockchain.js';
import { bitcoinToSats } from '../utils.js';
import { getDataToSign, getStacksAddressFromSignature } from '../structured-data.js';
import * as btc from '@scure/btc-signer';
import { hex } from '@scure/base';
import { network } from '../config.js';
import { hashMessage } from '@stacks/encryption';

export async function fetchRawTx(txid:string, verbose:boolean) {
let dataString = `{"jsonrpc":"1.0","id":"curltext","method":"getrawtransaction","params":["${txid}", ${verbose}]}`;
Expand Down Expand Up @@ -54,17 +45,16 @@ async function decodePegInOutputs(outputs:any) {
pegType: 'pegin',
opType: res.opType,
stxAddress: res.address,
amountSats,
amountSats,
sbtcWallet
}
return pegIn;
} catch (err) {
return await decodePegOutOutputs(outputs);
}
}
}

function readStacksAddressForPegIn(scriptPubKey:any):{opType:string, address:string} {
console.log('attemptStacksAddressForPegIn:scriptPubKey ', util.inspect(scriptPubKey, false, null, true /* enable colors */));
if (scriptPubKey.type === 'nulldata') {
// op_return?
const addrHex = scriptPubKey.asm.split(' ')[1]
Expand All @@ -76,63 +66,122 @@ function readStacksAddressForPegIn(scriptPubKey:any):{opType:string, address:str
}
}

async function decodePegOutOutputs(outputs:any) {
console.log('decodePegOutOutputs:scriptPubKey ', util.inspect(outputs[0].scriptPubKey, false, null, true /* enable colors */));
async function decodePegOutOutputs(outputs:any) {
console.log('decodePegOutOutputs:outputs ', util.inspect(outputs, false, null, true /* enable colors */));
// dust amount is sent to sbtc wallet in output 1
let parsed = parseOutputs(outputs);
console.log('decodePegOutOutputs:parsed ', util.inspect(parsed, false, null, true /* enable colors */));
const dataToSign = getDataToSign(parsed.amountSats, parsed.sbtcWallet);
const msgHash = hashMessage(dataToSign.toString('hex'));
console.log('msgHash: ' + msgHash);
const stxAddress = getStacksAddressFromSignature(msgHash, parsed.signature.toString('binary'), parsed.compression);
parsed.stxAddress = (network === 'testnet') ? stxAddress.tp2pkh : stxAddress.mp2pkh;
console.log('decodePegOutOutputs:stxAddress ', util.inspect(stxAddress, false, null, true /* enable colors */));
return parsed;
}

type parsedDataType = {
pegType: string;
opType: string;
stxAddress?: string;
sbtcWallet: string;
signature: Buffer;
compression: number,
amountSats: number;
dustAmount: number;
};

export function parseOutputs(outputs:Array<any>):parsedDataType {
if (outputs[0].scriptPubKey.type === 'nulldata') {
// op_return
const data = Buffer.from(outputs[0].scriptPubKey.hex, 'hex');
//console.log('buffer ', util.inspect(data.toString('hex'), false, null, true /* enable colors */));
const signature = data.subarray(12);
const pegOutAmount = data.subarray(3,11).readUInt32LE();
const stxAddress = await getOutput2ScriptPubKey(pegOutAmount, outputs[1].scriptPubKey.address, signature);
const sig = data.subarray(12);
const amt = data.subarray(3,11).readUInt32LE();
console.log('sif: ' + sig);
console.log('amt: ' + data.subarray(3,13).toString('hex'));
console.log('amt: ' + amt);
return {
pegType: 'pegout',
opType: 'return',
stxAddress,
signature: (signature) ? signature.toString('hex') : '',
amountSats: pegOutAmount,
compression: 0,
sbtcWallet: outputs[1].scriptPubKey.address,
signature: sig,
amountSats: amt,
dustAmount: bitcoinToSats(outputs[1].value)
};
} else {
}
} else if (outputs[0].scriptPubKey.type === 'nonstandard') {
// op_drop
const dropData = outputs[0].scriptPubKey.asm.split(' ')[0]
const data = Buffer.from(dropData, 'hex');
const pegOutAmount = data.subarray(0,8).readUInt32LE();
const signature = data.subarray(9);
const stxAddress = await getOutput2ScriptPubKey(pegOutAmount, outputs[0].scriptPubKey.address, signature);
const net = (network === 'testnet') ? btc.TEST_NETWORK : btc.NETWORK;
const asm = outputs[0].scriptPubKey.asm.split(' ');
const script = asm[4]

const asmScript = btc.Script.encode(['DUP', 'HASH160', Buffer.from(script, 'hex'), 'EQUALVERIFY', 'CHECKSIG'])

console.log('getOpDropP2shScript:asmScript: ', asmScript)

console.log('getOpDropP2shScript:script : ', script);
const addr = btc.OutScript.decode(Uint8Array.from(Buffer.from(script, 'hex')));
const addr1 = btc.Address(net).encode(addr);
console.log('getOpDropP2shScript:addr : ', addr);
console.log('getOpDropP2shScript:addr1 : ', addr1);









//const p2pkh = outputs[0].scriptPubKey.asm.split(' ').slice(2);
//const encscript = btc.Script.encode(fromASM(p2pkh));
//console.log('encscript: ', encscript)
//const script = btc.OutScript.decode(Buffer.from(outputs[0].scriptPubKey.asm))
//const addr = outputs[1].scriptPubKey.address;
//console.log('addr.encode(script): ', addr.encode(script))
//console.log('script: ', script)
//const data = Buffer.from(outputs[1].scriptPubKey.hex, 'hex');
//const dropData = outputs[0].scriptPubKey.asm.split(' ')[0]
//const data = Buffer.from(dropData, 'hex');
//const pegOutAmount = data.subarray(0,8).readUInt32LE();
//const signature = data.subarray(9);

const data = Buffer.from(outputs[0].scriptPubKey.hex, 'hex');
const amt = data.subarray(2,10).readUInt32LE();
const sig = data.subarray(11, 141);
console.log('sif: ' + sig);
console.log('amt: ' + data.subarray(2,10).toString('hex'));
console.log('amt: ' + amt);
return {
pegType: 'pegout',
opType: 'drop',
stxAddress: '',
signature: (signature) ? signature.toString('hex') : '',
amountSats: pegOutAmount,
sbtcWallet: outputs[0].scriptPubKey.address,
dustAmount: bitcoinToSats(outputs[0].value)
};
compression: 1,
sbtcWallet: outputs[1].scriptPubKey.address,
signature: sig,
amountSats: amt,
dustAmount: bitcoinToSats(outputs[1].value)
}
} else {
console.log('Error parsing : ', outputs)
throw new Error('Outputs not parsable');
}
}

export async function getOutput2ScriptPubKey(amount:number, sbtcWalletAddress:string, signature:Buffer):Promise<string> {
console.log('getOutput2ScriptPubKey:');
//console.log('btc:', btc);
//let btc = await import('../../../node_modules/micro-btc-signer/index');
//let btc = await import('micro-btc-signer');
let btc = await import('@scure/btc-signer');
console.log('btc:', btc);
//const {TEST_NETWORK, NETWORK,Address,OutScript} = await import('@scure/btc-signer');
//console.log('OutScript:', OutScript);
const amtBuf = Buffer.alloc(9);
amtBuf.writeUInt32LE(amount, 0);
const net = (network === 'testnet') ? btc.TEST_NETWORK : btc.NETWORK;
//console.log('btc1 ', util.inspect(btc, false, null, true /* enable colors */));
const script = btc.OutScript.encode(btc.Address(net).decode(sbtcWalletAddress))
console.log('decodePegOutOutputs ', util.inspect(Buffer.from(script).toString('hex'), false, null, true /* enable colors */));
const scriptBuf = Buffer.from(script);
const data = Buffer.concat([amtBuf, scriptBuf]);
console.log('decodePegOutOutputs ', util.inspect(data, false, null, true /* enable colors */));
//return { script: data }
const stxAddress = getStacksAddressFromSignature(data.toString('hex'), signature.toString('hex'));
return (network === 'testnet') ? stxAddress.tp2pkh : stxAddress.mp2pkh;
export function fromASM(asm:string) {
const ops = asm.split(' ');
const out = [];
for (const op of ops) {
if (op.startsWith('OP_')) {
let opName:any = op.slice(3);
if (opName === 'FALSE') opName = 0;
if (opName === 'TRUE') opName = 1;
// Handle numeric opcodes
if (String(Number(opName)) === opName) opName = +opName;
else if (btc.OP[opName] === undefined) throw new Error(`Wrong opcode='${op}'`);
out.push(opName);
} else {
out.push(hex.decode(op));
}
}
return out;
}

4 changes: 2 additions & 2 deletions bridge-api/src/lib/bitcoin/rpc_wallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BASE_URL, OPTIONS, handleError } from '../../controllers/BitcoinRPCController'
import { BASE_URL, OPTIONS, handleError } from '../../controllers/BitcoinRPCController.js'
import fetch from 'node-fetch';
import type { FeeEstimateResponse } from '../../controllers/BitcoinRPCController'
import type { FeeEstimateResponse } from '../../controllers/BitcoinRPCController.js'
import util from 'util'

export async function listUnspent() {
Expand Down
9 changes: 7 additions & 2 deletions bridge-api/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import {config as configDotenv} from 'dotenv'
import path from 'path';
import { fileURLToPath } from 'url';
import {config, config as configDotenv} from 'dotenv'
import {resolve} from 'path'
import type { IStringToStringDictionary } from '../controllers/ConfigController';
import type { IStringToStringDictionary } from '../controllers/ConfigController.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

switch(process.env.NODE_ENV) {
case "development":
Expand Down
3 changes: 2 additions & 1 deletion bridge-api/src/lib/data/sbtc_events_model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mongoose from "mongoose";
import { mongoUrl } from '../config';
import { mongoUrl } from '../config.js';
/** */
mongoose.set('strictQuery', true);

Expand Down Expand Up @@ -28,6 +28,7 @@ const SbtcEventSchema = new Schema({
amountSats: Number,
signature: String,
dustAmount: Number,
compression: Number,
burnBlockHeight: { type : Number , required : true },
sbtcWallet: { type : String , required : true },
}
Expand Down
Loading

0 comments on commit a40383e

Please sign in to comment.