Skip to content

Commit

Permalink
Feat/268 add analysis for fulfillment btc tx (#292)
Browse files Browse the repository at this point in the history
* filter and paging end points

* filter and paging end points

* added filter for my transactions and paging for all tranasactions
  • Loading branch information
mijoco-btc authored Oct 30, 2023
1 parent 354ad51 commit c44bbc7
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 126 deletions.
115 changes: 28 additions & 87 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@
"@stacks/transactions": "^6.7.0",
"@sveltejs/adapter-static": "^2.0.2",
"flowbite": "^1.8.1",
"flowbite-svelte-icons": "^0.4.4",
"micro-packed": "^0.3.2",
"sats-connect": "^1.1.1",
"sbtc-bridge-lib": "^1.1.18",
"sbtc-bridge-lib": "^1.1.19",
"sockjs": "^0.3.24",
"svelte-local-storage-store": "^0.5.0",
"svelte-qrcode": "^1.0.0",
Expand Down
8 changes: 4 additions & 4 deletions src/lib/bridge_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,12 @@ export async function findSbtcEventByBitcoinAddress(bitcoinAddress:string):Promi
return pegins;
}

export async function findSbtcEventsByPage(page:number):Promise<Array<SbtcClarityEvent>> {
const path = addNetSelector(CONFIG.VITE_BRIDGE_API + '/events/' + page);
export async function findSbtcEventsByPage(page:number, limit:number):Promise<{ results: Array<SbtcClarityEvent>, events:number}> {
const path = addNetSelector(CONFIG.VITE_BRIDGE_API + '/events/find-by/page/' + page + '/' + limit);
const response = await fetchCatchErrors(path);
if (response.status !== 200) {
console.log('Request failed to url: ' + path);
return [];
return { results: [], events:0};
}
const pegins = await extractResponse(response);
return pegins;
Expand All @@ -255,7 +255,7 @@ export async function fetchCommitments(btcAddress:string, stxAddress:string, sbt
}

export async function findSbtcEventsByFilter(name:string, value:string):Promise<Array<SbtcClarityEvent>> {
const path = addNetSelector(CONFIG.VITE_BRIDGE_API + '/events/filter/' + name + '/' + value);
const path = addNetSelector(CONFIG.VITE_BRIDGE_API + '/events/find-by/filter/' + name + '/' + value);
const response = await fetchCatchErrors(path);
if (response.status !== 200) {
console.log('Request failed to url: ' + path);
Expand Down
78 changes: 78 additions & 0 deletions src/lib/components/transactions/Paging.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import { Pagination } from 'flowbite-svelte';
import { ChevronLeftOutline, ChevronRightOutline, ListMusicOutline } from 'flowbite-svelte-icons';
import { createEventDispatcher, onMount } from "svelte";
const dispatch = createEventDispatcher();
export let totalEvents:number;
export let limit:number;
export let numPages:number;
let inited = false;
let pages:Array<{name:string, href:string, active:boolean}> = [];
$: activeUrl = $page.url.searchParams.get('page');
$: {
pages.forEach((page) => {
let splitUrl = page.href.split('?');
let queryString = splitUrl.slice(1).join('?');
const hrefParams = new URLSearchParams(queryString);
let hrefValue = hrefParams.get('page');
if (hrefValue === activeUrl) {
page.active = true;
} else {
page.active = false;
}
});
pages = pages;
}
const previous = () => {
const current = ($page.url.searchParams.has('page')) ? Number($page.url.searchParams.get('page')) : 1;
if (current <= 1) return
goto('/transactions?page=' + (current - 1))
};
const next = () => {
const current = ($page.url.searchParams.has('page')) ? Number($page.url.searchParams.get('page')) : 0;
if (current >= numPages) return
goto('/transactions?page=' + (current + 1))
};
afterNavigate((nav) => {
const mypage = ($page.url.searchParams.size === 0) ? 0 : Number($page.url.searchParams.get('page'))
dispatch("fetch_page", { page: mypage - 1 });
})
onMount(async () => {
let active = false;
for (let i=0; i < numPages; i++) {
let name = Number(i+1)
if ((i === 0 && ($page.url.searchParams.size === 0))) active = true
else if ((i+1) === Number($page.url.searchParams.get('page'))) active = true
pages.push({name: String(name), href: '/transactions?page=' + (i+1), active})
active = false;
}
inited = true;
})
</script>

{#if totalEvents > 0 && inited}
<div class="">
<div class="">
<Pagination {pages} on:previous={previous} on:next={next} icon>
<svelte:fragment slot="prev">
<span class="sr-only">Previous</span>
<ChevronLeftOutline class="w-2.5 h-2.5" />
</svelte:fragment>
<svelte:fragment slot="next">
<span class="sr-only">Next</span>
<ChevronRightOutline class="w-2.5 h-2.5" />
</svelte:fragment>
</Pagination>
</div>
</div>
{/if}
50 changes: 42 additions & 8 deletions src/lib/components/transactions/SbtcEventData.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
<script lang="ts">
import { explorerBtcAddressUrl, explorerBtcTxUrl, explorerTxUrl } from '$lib/utils';
import { fmtNumber, satsToBitcoin, truncate, type SbtcClarityEvent } from 'sbtc-bridge-lib';
import { explorerBtcAddressUrl, explorerBtcTxUrl, explorerTxUrl, parseFulfilPayloadFromOutput } from '$lib/utils';
import { fmtNumber, satsToBitcoin, truncate, type SbtcClarityEvent, getAddressFromOutScript } from 'sbtc-bridge-lib';
import { onMount } from 'svelte';
import ArrowUpRight from '../shared/ArrowUpRight.svelte';
import * as btc from '@scure/btc-signer';
import { hex } from '@scure/base';
import { fetchAddressTransactions, fetchTransaction, fetchTransactionHex } from '$lib/bridge_api';
import { CONFIG } from '$lib/config';
export let sbtcEvent:SbtcClarityEvent;
let bitcoinTxId:string;
let recipient:string;
let amount:number;
const getType = (eventType:string|undefined) => {
return (eventType === 'mint') ? 'deposit' : 'withdrawal'
const tx = getFulfil()
return (eventType === 'mint') ? 'deposit' : 'withdrawal'
}
const getFulfil = async () => {
if (sbtcEvent.payloadData.eventType === 'burn') {
const txIn = await fetchTransaction(sbtcEvent.bitcoinTxid.payload.value.split('x')[1])
const tx:btc.Transaction = btc.Transaction.fromRaw(hex.decode(txIn.hex), {allowUnknowInput:true, allowUnknowOutput: true, allowUnknownOutputs: true, allowUnknownInputs: true})
recipient = getAddressFromOutScript(CONFIG.VITE_NETWORK, tx.getOutput(1).script as Uint8Array)
amount = Number(tx.getOutput(1).amount)
const txs:Array<any> = await fetchAddressTransactions(recipient);
let fulfilTx:any;
for (let thisTx of txs) {
const vout0 = thisTx.vout[0]
if (vout0.scriptpubkey_type === 'op_return' && (vout0.scriptpubkey.indexOf('543221') > -1 || vout0.scriptpubkey.indexOf('583221') > -1)) {
const txHex = await fetchTransaction(thisTx.txid)
const tx:btc.Transaction = btc.Transaction.fromRaw(hex.decode(txHex.hex), {allowUnknowInput:true, allowUnknowOutput: true, allowUnknownOutputs: true, allowUnknownInputs: true})
parseFulfilPayloadFromOutput(CONFIG.VITE_NETWORK, tx)
const vout1 = thisTx.vout[1]
}
}
console.log(fulfilTx)
}
}
onMount(() => {
bitcoinTxId = sbtcEvent.bitcoinTxid.payload.value.split('x')[1]
})
</script>
Expand All @@ -28,6 +57,15 @@ onMount(() => {
<div class="w-1/5">Stacks Tx:</div>
<div class="w-4/5"><a class="" href={explorerTxUrl(sbtcEvent.txid)} target="_blank" rel="noreferrer">{(sbtcEvent.txid)}</a></div>
</div>
<div class="flex">
{#if recipient}
<div class="w-1/5">Recipient</div>
<div class="w-4/5">{recipient}</div>
{:else}
<div class="w-1/5">Sender</div>
<div class="w-4/5">{sbtcEvent.payloadData.spendingAddress}</div>
{/if}
</div>
<div class="flex">
<div class="w-1/5">Amount:</div>
<div class="w-4/5">{satsToBitcoin(sbtcEvent.payloadData.amountSats)}</div>
Expand All @@ -40,10 +78,6 @@ onMount(() => {
<div class="w-1/5">Block height</div>
<div class="w-4/5">{fmtNumber(sbtcEvent.payloadData.burnBlockHeight)}</div>
</div>
<div class="flex">
<div class="w-1/5">Tx Index</div>
<div class="w-4/5">{sbtcEvent.payloadData.txIndex}</div>
</div>
</div>


6 changes: 4 additions & 2 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ const MAINNET_CONFIG = {
export let CONFIG = MAINNET_CONFIG;

export function setConfig(network:string) {
if (!network) network = 'testnet'
if (network.indexOf('=') > -1) network = network.split('=')[1]
if (!network || network.indexOf('chain=') === -1) network = 'testnet'
else if (network.indexOf('devnet') === -1) network = 'devnet'
else if (network.indexOf('testnet') === -1) network = 'testnet'
else if (network.indexOf('mainnet') === -1) network = 'mainnet'
const mode = import.meta.env.MODE
if (mode === 'development') {
if (network === 'testnet' || network === 'mainnet') CONFIG = DEV_TESTNET_CONFIG;
Expand Down
56 changes: 54 additions & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CONFIG } from '$lib/config';
import * as btc from '@scure/btc-signer';
import * as secp from '@noble/secp256k1';
import type { AddressMempoolObject, SbtcClarityEvent } from 'sbtc-bridge-lib'
import { parseDepositPayload, type AddressMempoolObject, type PayloadType, type SbtcClarityEvent, getAddressFromOutScript, parseWithdrawPayload, MAGIC_BYTES_TESTNET, MAGIC_BYTES_MAINNET } from 'sbtc-bridge-lib'
import type { BridgeTransactionType } from 'sbtc-bridge-lib'
import { hex } from '@scure/base';
import { hash160 } from '@stacks/transactions';
Expand Down Expand Up @@ -252,4 +252,56 @@ export function convertOutputsBlockCypher(blockCypherTx:any, peginRequest:Bridge
]
}
*/
}
}

export function parseFulfilPayloadFromOutput(network:string, tx:btc.Transaction):PayloadType {
const out0 = tx.getOutput(0)
let d1 = out0.script?.subarray(5) as Uint8Array // strip the op type and data length
let witnessData = getMagicAndOpCode(d1);
if (witnessData.opcode !== '3C' && witnessData.opcode !== '3E') {
d1 = out0.script?.subarray(2) as Uint8Array // strip the op type and data length
witnessData = getMagicAndOpCode(d1);
}
witnessData.txType = btc.OutScript.decode(out0.script as Uint8Array).type;

let innerPayload:PayloadType = {} as PayloadType;
if (witnessData.opcode === '3C') {
innerPayload = parseDepositPayload(d1);
innerPayload.sbtcWallet = getAddressFromOutScript(network, tx.getOutput(1).script as Uint8Array)
//const inScript = btc.RawInput.encode({
// index: tx.getInput(0).index || 0,
// sequence: tx.getInput(0).sequence || 0,
// txid: tx.getInput(0).txid as Uint8Array,
// finalScriptSig: tx.getInput(0).finalScriptSig as Uint8Array,
//});
if (tx.outputsLength > 2) innerPayload.spendingAddress = getAddressFromOutScript(network, tx.getOutput(2).script!)
console.log('parsePayloadFromTransaction:spendingAddress: ' + innerPayload.spendingAddress)
return innerPayload;
} else if (witnessData.opcode.toUpperCase() === '3E') {
const recipient = getAddressFromOutScript(network, tx.getOutput(1).script as Uint8Array)
try {
innerPayload = parseWithdrawPayload(network, hex.encode(d1), recipient, 'vrs')
} catch (err:any) {
innerPayload = parseWithdrawPayload(network, hex.encode(d1), recipient, 'rsv')
}
innerPayload.spendingAddress = getAddressFromOutScript(network, tx.getOutput(1).script!);
if (tx.outputsLength > 2) innerPayload.sbtcWallet = getAddressFromOutScript(network, tx.getOutput(2).script as Uint8Array)
innerPayload.spendingAddress = recipient
return innerPayload;
} else {
throw new Error('Wrong opcode : expected: 3E or 3C : recieved: ' + witnessData.opcode)
}
}
export function getMagicAndOpCode(d1: Uint8Array): {magic?:string; opcode:string; txType? :string; } {
if (!d1 || d1.length < 2) throw new Error('no magic data passed');
const magic = hex.encode(d1.subarray(0,2));
if (magic === MAGIC_BYTES_TESTNET || magic === MAGIC_BYTES_MAINNET) {
return {
magic: magic.toUpperCase(),
opcode: hex.encode(d1.subarray(2,3)).toUpperCase()
}
}
return {
opcode: hex.encode(d1.subarray(0,1)).toUpperCase()
}
}
Loading

0 comments on commit c44bbc7

Please sign in to comment.