diff --git a/apps/demo.lasereyes.build/components/WalletCard.tsx b/apps/demo.lasereyes.build/components/WalletCard.tsx index 0b28d6b..9cef8d0 100644 --- a/apps/demo.lasereyes.build/components/WalletCard.tsx +++ b/apps/demo.lasereyes.build/components/WalletCard.tsx @@ -21,6 +21,7 @@ import { OP_NET, ProviderType, SPARROW, + SendArgs, } from '@omnisat/lasereyes' import { Card, @@ -42,6 +43,13 @@ import { ImNewTab } from 'react-icons/im' import { cn } from '@/lib/utils' import { useUtxos } from '@/hooks/useUtxos' import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" const WalletCard = ({ wallet, @@ -60,10 +68,10 @@ const WalletCard = ({ setSignedPsbt: ( psbt: | { - signedPsbtHex: string - signedPsbtBase64: string - txId?: string - } + signedPsbtHex: string + signedPsbtBase64: string + txId?: string + } | undefined ) => void }) => { @@ -94,8 +102,10 @@ const WalletCard = ({ signMessage, signPsbt, inscribe, + send, pushPsbt, switchNetwork, + getMetaBalances } = useLaserEyes() const [hasError, setHasError] = useState(false) @@ -110,6 +120,19 @@ const WalletCard = ({ 'Inscribed 100% clientside with Laser Eyes' ) + const [runes, setRunes] = useState<{ + balance: string; + symbol: string; + name: string; + }[] | undefined>() + const [selectedRune, setSelectedRune] = useState<{ + balance: string; + symbol: string; + name: string; + } | undefined>(undefined); + const [runeToAddress, setRuneToAddress] = useState('') + const [runeAmount, setRuneAmount] = useState('') + const hasWallet = { unisat: hasUnisat, xverse: hasXverse, @@ -144,12 +167,12 @@ const WalletCard = ({ paymentAddress, paymentPublicKey, network as - | typeof MAINNET - | typeof TESTNET - | typeof TESTNET4 - | typeof SIGNET - | typeof FRACTAL_MAINNET - | typeof FRACTAL_TESTNET + | typeof MAINNET + | typeof TESTNET + | typeof TESTNET4 + | typeof SIGNET + | typeof FRACTAL_MAINNET + | typeof FRACTAL_TESTNET ) .then((psbt) => { if (psbt && psbt.toHex() !== unsigned) { @@ -182,7 +205,14 @@ const WalletCard = ({ setUnsigned(undefined) }, [network]) - const send = async () => { + useEffect(() => { + if (address) { + getMetaBalances("runes").then(setRunes) + setRuneToAddress(address) + } + }, [address]) + + const sendBtc = async () => { try { if (balance! < 1500) { throw new Error('Insufficient funds') @@ -405,6 +435,42 @@ const WalletCard = ({ } }, [inscribe, inscriptionText, network]) + + const sendRune = async () => { + try { + if (!selectedRune) throw new Error('No rune selected') + if (!address) throw new Error('No address available') + if (!runeToAddress) throw new Error('No destination address provided') + if (!runeAmount) throw new Error('No amount specified') + + const txid = await send("runes", { + fromAddress: address, + toAddress: runeToAddress, + amount: Number(runeAmount), + runeId: selectedRune.name, + } as SendArgs) + + toast.success( + + View on mempool.space + + {txid} + + + ) + } catch (error) { + if (error instanceof Error) { + toast.error(error.message) + } + } + } + return ( (!isConnected ? null : send())} + onClick={() => (!isConnected ? null : sendBtc())} > send BTC @@ -589,6 +655,88 @@ const WalletCard = ({ > inscribe + +
+
+ + + setRuneToAddress(e.target.value)} + /> + setRuneAmount(e.target.value)} + /> + +
diff --git a/packages/lasereyes-core/package.json b/packages/lasereyes-core/package.json index d984c37..88b032f 100644 --- a/packages/lasereyes-core/package.json +++ b/packages/lasereyes-core/package.json @@ -1,7 +1,7 @@ { "name": "@omnisat/lasereyes-core", "private": false, - "version": "0.0.59", + "version": "0.0.60-rc.1", "type": "module", "main": "./dist/index.umd.cjs", "module": "./dist/index.js", @@ -31,6 +31,7 @@ "@bitcoinerlab/secp256k1": "^1.1.1", "@cmdcode/crypto-utils": "^2.4.6", "@cmdcode/tapscript": "^1.4.6", + "@magiceden-oss/runestone-lib": "^1.0.2", "@nanostores/persistent": "^0.10.2", "@orangecrypto/orange-connect": "^1.2.2", "axios": "^1.7.7", diff --git a/packages/lasereyes-core/src/client/index.ts b/packages/lasereyes-core/src/client/index.ts index 40d5e53..20a0d99 100644 --- a/packages/lasereyes-core/src/client/index.ts +++ b/packages/lasereyes-core/src/client/index.ts @@ -1,6 +1,5 @@ import { MapStore, WritableAtom, keepMount, listenKeys } from 'nanostores' - -import { Config, ContentType, NetworkType, ProviderType } from '../types' +import { BTCSendArgs, Config, ContentType, NetworkType, Protocol, ProviderType, RuneSendArgs } from '../types' import { LEATHER, MAGIC_EDEN, @@ -340,6 +339,27 @@ export class LaserEyesClient { } } + async send(protocol: Protocol, sendArgs: BTCSendArgs | RuneSendArgs) { + if (!this.$store.get().provider) return + if (this.$providerMap[this.$store.get().provider!]) { + try { + return await this.$providerMap[this.$store.get().provider!]?.send( + protocol, + sendArgs + ) + } catch (error) { + if (error instanceof Error) { + if (error.message.toLowerCase().includes('not implemented')) { + throw new Error( + "The connected wallet doesn't support sending stuff..." + ) + } + } + throw error + } + } + } + async getPublicKey() { if (!this.$store.get().provider) return if (this.$providerMap[this.$store.get().provider!]) { @@ -377,6 +397,30 @@ export class LaserEyesClient { } } + async getMetaBalances(protocol: Protocol) { + if (!this.$store.get().provider) return + if (this.$providerMap[this.$store.get().provider!]) { + try { + if (!protocol) { + throw new Error('No protocol provided') + } + + const balances = + await this.$providerMap[this.$store.get().provider!]!.getMetaBalances(protocol) + // TODO: Decide if we want to store these balances + // this.$store.setKey(`${protocol}Balances`, JSON.stringify(balances)) + return balances + } catch (error) { + if (error instanceof Error) { + if (error.message.toLowerCase().includes('not implemented')) { + throw new Error("The connected wallet doesn't support getBalance") + } + } + throw error + } + } + } + async getInscriptions(offset?: number, limit?: number) { if (!this.$store.get().provider) return if (this.$providerMap[this.$store.get().provider!]) { diff --git a/packages/lasereyes-core/src/client/providers/index.ts b/packages/lasereyes-core/src/client/providers/index.ts index 4cc9785..4ca9b28 100644 --- a/packages/lasereyes-core/src/client/providers/index.ts +++ b/packages/lasereyes-core/src/client/providers/index.ts @@ -1,6 +1,6 @@ import { MapStore, WritableAtom } from 'nanostores' import { LaserEyesStoreType } from '../types' -import { Config, ContentType, NetworkType, ProviderType } from '../../types' +import { BTCSendArgs, Config, ContentType, NetworkType, Protocol, ProviderType, RuneSendArgs } from '../../types' import { LaserEyesClient } from '..' import { inscribeContent } from '../../lib/inscribe' import { broadcastTx, getBTCBalance } from '../../lib/helpers' @@ -12,6 +12,9 @@ import { TESTNET, TESTNET4, } from '../../constants' +import { BTC, RUNES } from '../../constants/protocols' +import { sendRune } from '../../lib/runes/psbt' +import { getAddressRunesBalances } from '../../lib/sandshrew' export const UNSUPPORTED_PROVIDER_METHOD_ERROR = new Error( "The connected wallet doesn't support this method..." @@ -35,7 +38,7 @@ export abstract class WalletProvider { this.initialize() } - disconnect(): void {} + disconnect(): void { } abstract initialize(): void @@ -73,6 +76,22 @@ export abstract class WalletProvider { return await getBTCBalance(paymentAddress, this.$network.get()) } + async getMetaBalances(protocol: Protocol): Promise { + switch (protocol) { + case BTC: + return await this.getBalance() + case RUNES: + const network = this.$network.get() + if (network !== MAINNET) { + throw new Error('Unsupported network') + } + + return await getAddressRunesBalances(this.$store.get().address) + default: + throw new Error('Unsupported protocol') + } + } + async getInscriptions(offset?: number, limit?: number): Promise { console.log('getInscriptions not implemented', offset, limit) throw UNSUPPORTED_PROVIDER_METHOD_ERROR @@ -90,17 +109,16 @@ export abstract class WalletProvider { broadcast?: boolean ): Promise< | { - signedPsbtHex: string | undefined - signedPsbtBase64: string | undefined - txId?: string - } + signedPsbtHex: string | undefined + signedPsbtBase64: string | undefined + txId?: string + } | undefined > async pushPsbt(_tx: string): Promise { let payload = _tx if (!payload.startsWith('02')) { - console.log('extracting tx...') const psbtObj = bitcoin.Psbt.fromHex(payload) payload = psbtObj.extractTransaction().toHex() } @@ -122,4 +140,35 @@ export abstract class WalletProvider { network: this.$network.get(), }) } + + async send(protocol: Protocol, sendArgs: BTCSendArgs | RuneSendArgs) { + switch (protocol) { + case BTC: + return await this.sendBTC(sendArgs.toAddress, sendArgs.amount) + case RUNES: + const network = this.$network.get() + if (network !== MAINNET) { + throw new Error('Unsupported network') + } + + const runeArgs = sendArgs as RuneSendArgs; + if (!runeArgs.runeId || !runeArgs.amount || !runeArgs.toAddress) { + throw new Error('Missing required parameters') + } + + return await sendRune({ + runeId: runeArgs.runeId, + amount: runeArgs.amount, + ordinalAddress: this.$store.get().address, + ordinalPublicKey: this.$store.get().publicKey, + paymentAddress: this.$store.get().paymentAddress, + paymentPublicKey: this.$store.get().paymentPublicKey, + toAddress: runeArgs.toAddress, + signPsbt: this.signPsbt.bind(this), + network + }) + default: + throw new Error('Unsupported protocol') + } + } } diff --git a/packages/lasereyes-core/src/client/providers/magic-eden.ts b/packages/lasereyes-core/src/client/providers/magic-eden.ts index 27ce86d..c050e44 100644 --- a/packages/lasereyes-core/src/client/providers/magic-eden.ts +++ b/packages/lasereyes-core/src/client/providers/magic-eden.ts @@ -265,10 +265,10 @@ export default class MagicEdenProvider extends WalletProvider { broadcast?: boolean | undefined ): Promise< | { - signedPsbtHex: string | undefined - signedPsbtBase64: string | undefined - txId?: string | undefined - } + signedPsbtHex: string | undefined + signedPsbtBase64: string | undefined + txId?: string | undefined + } | undefined > { console.log('signPsbt', psbtBase64, _finalize, broadcast) @@ -369,19 +369,19 @@ export default class MagicEdenProvider extends WalletProvider { if (_finalize || broadcast) { signedPsbt.finalizeAllInputs() - const signedTx = signedPsbt.extractTransaction() - signedPsbtHex = signedTx.toHex() - if (broadcast) { - txId = await this.pushPsbt(signedPsbtHex) - return { - signedPsbtHex, - signedPsbtBase64, - txId, - } - } + // const signedTx = signedPsbt.extractTransaction() + // signedPsbtHex = signedTx.toHex() + // if (broadcast) { + // txId = await this.pushPsbt(signedPsbtHex) + // return { + // signedPsbtHex, + // signedPsbtBase64, + // txId, + // } + // } return { - signedPsbtHex, + signedPsbtHex: signedPsbt.toHex(), signedPsbtBase64, txId, } diff --git a/packages/lasereyes-core/src/constants/protocols.ts b/packages/lasereyes-core/src/constants/protocols.ts new file mode 100644 index 0000000..64278ef --- /dev/null +++ b/packages/lasereyes-core/src/constants/protocols.ts @@ -0,0 +1,4 @@ +export const BTC = 'btc' +export const BRC20 = 'brc20' +export const RUNES = 'runes' +export const ALKANES = 'alkanes' diff --git a/packages/lasereyes-core/src/lib/btc.ts b/packages/lasereyes-core/src/lib/btc.ts new file mode 100644 index 0000000..02c78d9 --- /dev/null +++ b/packages/lasereyes-core/src/lib/btc.ts @@ -0,0 +1,140 @@ +import * as bitcoin from 'bitcoinjs-lib' +import * as ecc2 from '@bitcoinerlab/secp256k1' +import { + getBitcoinNetwork, +} from './helpers' +import * as bip39 from 'bip39' +import { + NetworkType, +} from '../types' +import { BIP32Factory, BIP32Interface } from 'bip32' +import { getTransactionMempoolSpace } from './mempool-space' +import { P2PKH, P2SH_P2WPKH, P2SH, P2WPKH, P2WSH, P2TR } from '../constants' + +const bip32 = BIP32Factory(ecc2) +bitcoin.initEccLib(ecc2) + +export async function generatePrivateKey(network: NetworkType) { + const entropy = crypto.getRandomValues(new Uint8Array(32)) + const mnemonic = bip39.entropyToMnemonic(Buffer.from(entropy)) + const seed = await bip39.mnemonicToSeed(mnemonic) + const root: BIP32Interface = bip32.fromSeed(seed, getBitcoinNetwork(network)) + return root?.derivePath("m/44'/0'/0'/0/0").privateKey +} + + +export const getAddressType = ( + address: string, + network: NetworkType +): string => { + try { + const btcNetwork = getBitcoinNetwork(network) + const decoded = bitcoin.address.fromBase58Check(address) + if (decoded.version === btcNetwork.pubKeyHash) return P2PKH + if (decoded.version === btcNetwork.scriptHash) { + const script = bitcoin.script.decompile(decoded.hash) + if (script && script.length === 2 && script[0] === bitcoin.opcodes.OP_0) { + return P2SH_P2WPKH + } + return P2SH + } + } catch (e) { + try { + const decoded = bitcoin.address.fromBech32(address) + if (decoded.version === 0 && decoded.data.length === 20) return P2WPKH + if (decoded.version === 0 && decoded.data.length === 32) return P2WSH + if (decoded.version === 1 && decoded.data.length === 32) return P2TR + } catch (e2) { + return 'unknown' + } + } + + return 'unknown' +} + +export function getPublicKeyHash( + address: string, + network: NetworkType +): Uint8Array { + const btcNetwork = getBitcoinNetwork(network) + const decoded = bitcoin.address.toOutputScript(address, btcNetwork) + return decoded +} + + +export function getRedeemScript( + paymentPublicKey: string, + network: NetworkType +) { + const p2wpkh = bitcoin.payments.p2wpkh({ + pubkey: Buffer.from(paymentPublicKey, 'hex'), + network: getBitcoinNetwork(network), + }) + + const p2sh = bitcoin.payments.p2sh({ + redeem: p2wpkh, + network: getBitcoinNetwork(network), + }) + return p2sh?.redeem?.output +} + +export async function waitForTransaction( + txId: string, + network: NetworkType +): Promise { + const timeout: number = 60000 + const startTime: number = Date.now() + while (true) { + try { + const tx: any = await getTransactionMempoolSpace(txId, network) + if (tx) { + console.log('Transaction found in mempool:', txId) + return true + } + + if (Date.now() - startTime > timeout) { + return false + } + + await new Promise((resolve) => setTimeout(resolve, 5000)) + } catch (error) { + if (Date.now() - startTime > timeout) { + return false + } + + await new Promise((resolve) => setTimeout(resolve, 5000)) + } + } +} + +export async function getOutputValueByVOutIndex( + commitTxId: string, + vOut: number, + network: NetworkType +): Promise { + const timeout: number = 60000 + const startTime: number = Date.now() + + while (true) { + try { + const rawTx: any = await getTransactionMempoolSpace(commitTxId, network) + + if (rawTx && rawTx.vout && rawTx.vout.length > 0) { + return Math.floor(rawTx.vout[vOut].value) + } + + if (Date.now() - startTime > timeout) { + return null + } + + await new Promise((resolve) => setTimeout(resolve, 5000)) + } catch (error) { + console.error('Error fetching transaction output value:', error) + if (Date.now() - startTime > timeout) { + return null + } + + await new Promise((resolve) => setTimeout(resolve, 5000)) + } + } +} diff --git a/packages/lasereyes-core/src/lib/helpers.ts b/packages/lasereyes-core/src/lib/helpers.ts index 052f94c..2a60d2a 100644 --- a/packages/lasereyes-core/src/lib/helpers.ts +++ b/packages/lasereyes-core/src/lib/helpers.ts @@ -1,5 +1,4 @@ import * as bitcoin from 'bitcoinjs-lib' -import { Buffer } from 'buffer' import { FRACTAL_MAINNET, @@ -13,7 +12,7 @@ import axios from 'axios' import { MempoolUtxo, NetworkType } from '../types' import { getMempoolSpaceUrl } from './urls' import * as ecc from '@bitcoinerlab/secp256k1' -import { P2PKH, P2SH, P2SH_P2WPKH, P2TR, P2WPKH, P2WSH } from '../constants' +import { getRedeemScript } from './btc' bitcoin.initEccLib(ecc) @@ -174,21 +173,6 @@ export async function createSendBtcPsbt( } } -export function getRedeemScript( - paymentPublicKey: string, - network: NetworkType -) { - const p2wpkh = bitcoin.payments.p2wpkh({ - pubkey: Buffer.from(paymentPublicKey, 'hex'), - network: getBitcoinNetwork(network), - }) - - const p2sh = bitcoin.payments.p2sh({ - redeem: p2wpkh, - network: getBitcoinNetwork(network), - }) - return p2sh?.redeem?.output -} export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) @@ -214,34 +198,6 @@ export async function broadcastTx( return response.data } -export const getAddressType = ( - address: string, - network: NetworkType -): string => { - try { - const btcNetwork = getBitcoinNetwork(network) - const decoded = bitcoin.address.fromBase58Check(address) - if (decoded.version === btcNetwork.pubKeyHash) return P2PKH - if (decoded.version === btcNetwork.scriptHash) { - const script = bitcoin.script.decompile(decoded.hash) - if (script && script.length === 2 && script[0] === bitcoin.opcodes.OP_0) { - return P2SH_P2WPKH - } - return P2SH - } - } catch (e) { - try { - const decoded = bitcoin.address.fromBech32(address) - if (decoded.version === 0 && decoded.data.length === 20) return P2WPKH - if (decoded.version === 0 && decoded.data.length === 32) return P2WSH - if (decoded.version === 1 && decoded.data.length === 32) return P2TR - } catch (e2) { - return 'unknown' - } - } - - return 'unknown' -} export const isTestnetNetwork = (network: NetworkType) => { return network === TESTNET || network === TESTNET4 || network === SIGNET diff --git a/packages/lasereyes-core/src/lib/inscribe.ts b/packages/lasereyes-core/src/lib/inscribe.ts index 5ccde73..f441927 100644 --- a/packages/lasereyes-core/src/lib/inscribe.ts +++ b/packages/lasereyes-core/src/lib/inscribe.ts @@ -6,26 +6,20 @@ import * as ecc2 from '@bitcoinerlab/secp256k1' import { broadcastTx, calculateValueOfUtxosGathered, - getAddressType, getAddressUtxos, getBitcoinNetwork, - getRedeemScript, } from './helpers' import { getCmDruidNetwork, MAINNET, P2SH, P2TR } from '../constants' -import axios from 'axios' -import { getMempoolSpaceUrl } from './urls' -import * as bip39 from 'bip39' import { ContentType, - MempoolTransactionResponse, MempoolUtxo, NetworkType, } from '../types' -import { BIP32Factory, BIP32Interface } from 'bip32' import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371' import { TEXT_PLAIN } from '../constants/content' +import { generatePrivateKey, waitForTransaction, getOutputValueByVOutIndex, getAddressType, getRedeemScript } from './btc' +import { getRecommendedFeesMempoolSpace } from './mempool-space' -const bip32 = BIP32Factory(ecc2) bitcoin.initEccLib(ecc2) export const inscribeContent = async ({ @@ -55,10 +49,10 @@ export const inscribeContent = async ({ network?: NetworkType ) => Promise< | { - signedPsbtHex: string | undefined - signedPsbtBase64: string | undefined - txId?: string - } + signedPsbtHex: string | undefined + signedPsbtBase64: string | undefined + txId?: string + } | undefined > network: NetworkType @@ -72,11 +66,11 @@ export const inscribeContent = async ({ const ixs = inscriptions ? inscriptions : Array(quantity).fill({ - content: contentBase64, - mimeType, - }) + content: contentBase64, + mimeType, + }) - const commitTx = await getCommitTx({ + const commitTx = await getCommitPsbt({ inscriptions: ixs, paymentAddress, paymentPublicKey, @@ -103,7 +97,7 @@ export const inscribeContent = async ({ const extracted = psbt.extractTransaction() const commitTxId = await broadcastTx(extracted.toHex(), network) if (!commitTxId) throw new Error('commit tx failed') - return await executeReveal({ + return await executeRevealTransaction({ inscriptions: ixs, ordinalAddress, privKey, @@ -115,7 +109,7 @@ export const inscribeContent = async ({ } } -export const getCommitTx = async ({ +export const getCommitPsbt = async ({ inscriptions, paymentAddress, paymentPublicKey, @@ -130,9 +124,9 @@ export const getCommitTx = async ({ isDry?: boolean }): Promise< | { - psbtHex: string - psbtBase64: string - } + psbtHex: string + psbtBase64: string + } | undefined > => { try { @@ -144,13 +138,13 @@ export const getCommitTx = async ({ if (contentSize > 390000) throw new Error('Content size is too large, must be less than 390kb') - const { fastestFee } = await getRecommendedFees(network) + const { fastestFee } = await getRecommendedFeesMempoolSpace(network) const pubKey = ecc.keys.get_pubkey(String(privKey), true) const psbt = new bitcoin.Psbt({ network: getBitcoinNetwork(network), }) - const { inscriberAddress } = createRevealAddressAndKeys( + const { inscriberAddress } = createInscriptionRevealAddressAndKeys( pubKey, inscriptions, network @@ -179,7 +173,7 @@ export const getCommitTx = async ({ } let accSats = 0 - const addressScript = await bitcoin.address.toOutputScript( + const addressScript = bitcoin.address.toOutputScript( paymentAddress, getBitcoinNetwork(network) ) @@ -235,7 +229,7 @@ export const getCommitTx = async ({ } } -export const executeReveal = async ({ +export const executeRevealTransaction = async ({ inscriptions, ordinalAddress, commitTxId, @@ -249,7 +243,7 @@ export const executeReveal = async ({ privKey: string network: NetworkType isDry?: boolean -}) => { +}): Promise => { try { const secKey = ecc.keys.get_seckey(privKey) const pubKey = ecc.keys.get_pubkey(privKey, true) @@ -302,23 +296,14 @@ export const executeReveal = async ({ } } -export async function generatePrivateKey(network: NetworkType) { - const entropy = crypto.getRandomValues(new Uint8Array(32)) - const mnemonic = bip39.entropyToMnemonic(Buffer.from(entropy)) - const seed = await bip39.mnemonicToSeed(mnemonic) - const root: BIP32Interface = bip32.fromSeed(seed, getBitcoinNetwork(network)) - return root?.derivePath("m/44'/0'/0'/0/0").privateKey -} - export const createInscriptionScript = ( pubKey: any, inscriptions: { content: string; mimeType: ContentType }[] ) => { const ec = new TextEncoder() const marker = ec.encode('ord') - const INSCRIPTION_SIZE = 546 // Constant for inscription size + const INSCRIPTION_SIZE = 546 - // Function to create content chunks for each inscription const createContentChunks = ( contentBase64: string, mimeType: ContentType @@ -331,7 +316,6 @@ export const createInscriptionScript = ( contentBuffer = Buffer.from(contentBase64, 'base64') } - // Split content into chunks of 520 bytes const contentChunks = [] for (let i = 0; i < contentBuffer.length; i += 520) { contentChunks.push(contentBuffer.slice(i, i + 520)) @@ -340,35 +324,25 @@ export const createInscriptionScript = ( return contentChunks } - // Construct the script starting with pubKey and OP_CHECKSIG const script: any = [pubKey, 'OP_CHECKSIG'] - - // Add envelopes for each inscription inscriptions.forEach((inscription, index) => { const { content, mimeType } = inscription const contentChunks = createContentChunks(content, mimeType) - - // Start the inscription envelope script.push('OP_0', 'OP_IF', marker, '01', ec.encode(mimeType), 'OP_0') - - // Add pointer logic only if it's not the first inscription if (index > 0) { const pointer = INSCRIPTION_SIZE * (index + 1) const pointerBuffer = Buffer.from([pointer]) - - script.push(Buffer.from([0x02])) // Pointer tag - script.push(pointerBuffer) // Pointer value in minimal format + script.push(Buffer.from([0x02])) + script.push(pointerBuffer) } - // Add content chunks and close the conditional block script.push(...contentChunks.map((chunk) => chunk), 'OP_ENDIF') }) - // Return the complete script with all envelopes return script } -export const createRevealAddressAndKeys = ( +export const createInscriptionRevealAddressAndKeys = ( pubKey: any, inscriptions: { content: string; mimeType: ContentType }[], network: NetworkType = MAINNET @@ -388,99 +362,3 @@ export const createRevealAddressAndKeys = ( } } -export async function getTransaction( - txId: string, - network: NetworkType = MAINNET -): Promise { - try { - return await axios - .get(`${getMempoolSpaceUrl(network)}/api/tx/${txId}`) - .then((res) => res.data) - } catch (e: any) { - throw e - } -} - -export async function getRawTransaction( - txId: string, - network: NetworkType = MAINNET -): Promise { - try { - return await axios - .get(`${getMempoolSpaceUrl(network)}/api/tx/${txId}/raw`) - .then((res) => res.data) - } catch (e: any) { - throw e - } -} - -export async function waitForTransaction( - txId: string, - network: NetworkType -): Promise { - const timeout: number = 60000 - const startTime: number = Date.now() - while (true) { - try { - const rawTx: any = await getTransaction(txId, network) - if (rawTx) { - console.log('Transaction found in mempool:', txId) - return true - } - - if (Date.now() - startTime > timeout) { - return false - } - - await new Promise((resolve) => setTimeout(resolve, 5000)) - } catch (error) { - if (Date.now() - startTime > timeout) { - return false - } - - await new Promise((resolve) => setTimeout(resolve, 5000)) - } - } -} - -export const getRecommendedFees = async (network: NetworkType) => { - return await axios - .get(`${getMempoolSpaceUrl(network)}/api/v1/fees/recommended`, { - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((res) => res.data) -} - -export async function getOutputValueByVOutIndex( - commitTxId: string, - vOut: number, - network: NetworkType -): Promise { - const timeout: number = 60000 - const startTime: number = Date.now() - - while (true) { - try { - const rawTx: any = await getTransaction(commitTxId, network) - - if (rawTx && rawTx.vout && rawTx.vout.length > 0) { - return Math.floor(rawTx.vout[vOut].value) - } - - if (Date.now() - startTime > timeout) { - return null - } - - await new Promise((resolve) => setTimeout(resolve, 5000)) - } catch (error) { - console.error('Error fetching transaction output value:', error) - if (Date.now() - startTime > timeout) { - return null - } - - await new Promise((resolve) => setTimeout(resolve, 5000)) - } - } -} diff --git a/packages/lasereyes-core/src/lib/mempool-space.ts b/packages/lasereyes-core/src/lib/mempool-space.ts new file mode 100644 index 0000000..4c10dd4 --- /dev/null +++ b/packages/lasereyes-core/src/lib/mempool-space.ts @@ -0,0 +1,48 @@ +import axios from "axios" +import { MAINNET } from "../constants" +import { NetworkType, MempoolTransactionResponse } from "../types" +import { getMempoolSpaceUrl } from "./urls" + + + +export async function getTransactionMempoolSpace( + txId: string, + network: NetworkType = MAINNET +): Promise { + try { + return await axios + .get(`${getMempoolSpaceUrl(network)}/api/tx/${txId}`) + .then((res) => res.data) + } catch (e: any) { + throw e + } +} + +export async function getRawTransactionMempoolSpace( + txId: string, + network: NetworkType = MAINNET +): Promise { + try { + return await axios + .get(`${getMempoolSpaceUrl(network)}/api/tx/${txId}/raw`) + .then((res) => res.data) + } catch (e: any) { + throw e + } +} + +export const getRecommendedFeesMempoolSpace = async (network: NetworkType): Promise<{ + fastestFee: number + halfHourFee: number + hourFee: number + economyFee: number + minimumFee: number +}> => { + return await axios + .get(`${getMempoolSpaceUrl(network)}/api/v1/fees/recommended`, { + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.data) +} diff --git a/packages/lasereyes-core/src/lib/runes.ts b/packages/lasereyes-core/src/lib/runes.ts new file mode 100644 index 0000000..9f1b591 --- /dev/null +++ b/packages/lasereyes-core/src/lib/runes.ts @@ -0,0 +1,110 @@ +import { encodeRunestone, RunestoneSpec } from '@magiceden-oss/runestone-lib' +import { encodeVarint } from './utils' + +export const createRuneMintScript = ({ + runeId, +}: { + runeId: string + pointer?: number +}) => { + const [blockStr, txStr] = runeId.split(':') + const runestone: RunestoneSpec = { + mint: { + block: BigInt(blockStr), + tx: parseInt(txStr, 10), + }, + } + return encodeRunestone(runestone).encodedRunestone +} + +export const createRuneEtchScript = ({ + pointer = 0, + runeName, + symbol, + divisibility, + perMintAmount, + premine = 0, + cap, + turbo, +}: { + pointer?: number + runeName: string + symbol: string + divisibility?: number + perMintAmount: number + cap?: number + premine?: number + turbo?: boolean +}) => { + const runeEtch = encodeRunestone({ + etching: { + divisibility, + premine: BigInt(premine), + runeName, + symbol, + terms: { + cap: cap ? BigInt(cap) : undefined, + amount: perMintAmount ? BigInt(perMintAmount) : undefined, + }, + turbo, + }, + pointer, + }).encodedRunestone + return runeEtch +} + +export const createRuneSendScript = ({ + runeId, + amount, + divisibility = 0, + sendOutputIndex = 1, + pointer = 0, +}: { + runeId: string + amount: number + divisibility?: number + sendOutputIndex?: number + pointer: number +}) => { + if (divisibility === 0) { + amount = Math.floor(amount) + } + const pointerFlag = encodeVarint(BigInt(22)).varint + const pointerVarint = encodeVarint(BigInt(pointer)).varint + const bodyFlag = encodeVarint(BigInt(0)).varint + const amountToSend = encodeVarint(BigInt(amount * 10 ** divisibility)).varint + const encodedOutputIndex = encodeVarint(BigInt(sendOutputIndex)).varint + const splitIdString = runeId.split(':') + const block = Number(splitIdString[0]) + const blockTx = Number(splitIdString[1]) + + const encodedBlock = encodeVarint(BigInt(block)).varint + const encodedBlockTxNumber = encodeVarint(BigInt(blockTx)).varint + + const runeStone = Buffer.concat([ + pointerFlag, + pointerVarint, + bodyFlag, + encodedBlock, + encodedBlockTxNumber, + amountToSend, + encodedOutputIndex, + ]) + + let runeStoneLength: string = runeStone.byteLength.toString(16) + + if (runeStoneLength.length % 2 !== 0) { + runeStoneLength = '0' + runeStone.byteLength.toString(16) + } + + const script: Buffer = Buffer.concat([ + Buffer.from('6a', 'hex'), + Buffer.from('5d', 'hex'), + Buffer.from(runeStoneLength, 'hex'), + runeStone, + ]) + return script +} + + + diff --git a/packages/lasereyes-core/src/lib/runes/psbt.ts b/packages/lasereyes-core/src/lib/runes/psbt.ts new file mode 100644 index 0000000..39344a9 --- /dev/null +++ b/packages/lasereyes-core/src/lib/runes/psbt.ts @@ -0,0 +1,289 @@ + +import * as bitcoin from "bitcoinjs-lib" +import { createRuneMintScript, createRuneSendScript } from "./scripts" +import { getRecommendedFeesMempoolSpace } from "../mempool-space" +import { MAINNET, P2SH, P2TR, } from "../../constants" +import { getRuneOutpoints } from "./utils" +import { broadcastTx, calculateValueOfUtxosGathered, estimateTxSize, getAddressUtxos, getBitcoinNetwork } from "../helpers" +import { getRuneById } from "../sandshrew" +import { NetworkType } from "../../types" +import { getAddressType, getRedeemScript } from "../btc" +import { toXOnly } from "bitcoinjs-lib/src/psbt/bip371" +// import { getAddressType, getRedeemScript } from "../btc" +// import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371' + +export const sendRune = async ({ + runeId, + amount, + ordinalAddress, + ordinalPublicKey, + paymentAddress, + paymentPublicKey, + toAddress, + signPsbt, + network = MAINNET, +}: { + runeId: string + amount: number + ordinalAddress: string + ordinalPublicKey: string + paymentAddress: string + paymentPublicKey: string + toAddress: string + signPsbt: ( + tx: string, + psbtHex: string, + psbtBase64: string, + finalize?: boolean, + broadcast?: boolean, + network?: NetworkType + ) => Promise< + | { + signedPsbtHex: string | undefined + signedPsbtBase64: string | undefined + txId?: string + } + | undefined + > + network: NetworkType +}): Promise => { + try { + const runeSendPsbt = await createRuneSendPsbt({ + fromAddress: ordinalAddress, + fromAddressPublicKey: ordinalPublicKey, + fromPaymentAddress: paymentAddress, + fromPaymentPublicKey: paymentPublicKey, + toAddress, + runeId, + amount, + network + }) + + if (!runeSendPsbt || !runeSendPsbt?.psbtHex) { + throw new Error("couldn't get commit tx") + } + + const runeSendTxHex = String(runeSendPsbt?.psbtHex) + const runeSendTxBase64 = String(runeSendPsbt?.psbtBase64) + const response = await signPsbt( + '', + runeSendTxHex, + runeSendTxBase64, + true, + false, + network + ) + if (!response) throw new Error('sign psbt failed') + const psbt = bitcoin.Psbt.fromHex(response?.signedPsbtHex || '') + const extracted = psbt.extractTransaction() + return await broadcastTx(extracted.toHex(), network) + } catch (e) { + throw e + } +} + + +export const createRuneSendPsbt = async ({ + fromAddress, + fromAddressPublicKey, + fromPaymentAddress, + fromPaymentPublicKey, + toAddress, + runeId, + amount, + network +}: { + fromAddress: string + fromAddressPublicKey: string + fromPaymentAddress: string + fromPaymentPublicKey: string + toAddress: string + runeId: string + amount: number + network: NetworkType +}): Promise<{ + psbtBase64: string + psbtHex: string +}> => { + try { + const { fastestFee: feeRate } = await getRecommendedFeesMempoolSpace(network) + const utxos = await getAddressUtxos(fromPaymentAddress, network) + let sortedUtxos = utxos.sort((a: { value: number }, b: { value: number }) => b.value - a.value).filter((utxo: { value: number }) => utxo.value > 3000) + if (sortedUtxos.length === 0) { + throw new Error('No utxos found') + } + + let psbt = new bitcoin.Psbt({ network: getBitcoinNetwork(network) }) + + let runeTotalSatoshis = 0 + const rune = await getRuneById(runeId) + const outpoints = await getRuneOutpoints({ runeId, address: fromAddress }) + const amountGathered = calculateValueOfUtxosGathered(sortedUtxos) + + const minFee = estimateTxSize(outpoints.length, 2, 4) + const calculatedFee = minFee * feeRate < 250 ? 250 : minFee * (feeRate) + let finalFee = calculatedFee + + let counter = 0 + for await (const runeOutput of outpoints) { + const { output, value, script } = runeOutput + const txSplit = output.split(':') + const txHash = txSplit[0] + const txIndex = txSplit[1] + psbt.addInput({ + hash: txHash, + index: parseInt(txIndex), + witnessUtxo: { + value: BigInt(value), + script: Buffer.from(script, 'hex'), + }, + tapInternalKey: toXOnly(Buffer.from(fromAddressPublicKey, 'hex')), + }) + + counter++ + runeTotalSatoshis += value + } + + const paymentAddressType = getAddressType(fromPaymentAddress, network) + for (let i = 0; i < sortedUtxos.length; i++) { + const script = bitcoin.address.toOutputScript(fromPaymentAddress, getBitcoinNetwork(MAINNET)) + const utxo = sortedUtxos[i] + + if (paymentAddressType === P2TR) { + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + value: BigInt(utxo.value), + script, + }, + tapInternalKey: toXOnly(Buffer.from(fromPaymentPublicKey, 'hex')), + }) + } + + if (paymentAddressType === P2SH) { + let redeemScript = getRedeemScript(fromPaymentPublicKey, network) + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + value: BigInt(utxo.value), + script, + }, + redeemScript, + }) + } + + if (paymentAddressType === "p2wpkh") { + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + value: BigInt(utxo.value), + script, + }, + }) + } + } + + const script = createRuneSendScript({ + runeId: rune.id, + amount, + divisibility: rune.entry.divisibility, + sendOutputIndex: 2, + pointer: 1, + }) + + const output = { script: script, value: BigInt(0) } + psbt.addOutput(output) + + const inscriptionSats = 546 + const changeAmount = + amountGathered - (finalFee + (inscriptionSats * 2)) + + psbt.addOutput({ + value: BigInt(inscriptionSats), + address: fromAddress, + }) + + psbt.addOutput({ + value: BigInt(inscriptionSats), + address: toAddress, + }) + + psbt.addOutput({ + address: fromAddress, + value: BigInt(changeAmount), + }) + + return { psbtBase64: psbt.toBase64(), psbtHex: psbt.toHex() } + } catch (error) { + throw error + } +} + +export const createRuneMintPsbt = async ({ + address, + runeId, +}: { + address: string + runeId: string +}) => { + try { + const network = "mainnet" + const { fastestFee: feeRate } = await getRecommendedFeesMempoolSpace(network) + const utxos = await getAddressUtxos(address, network) + + const minFee = 300 + const inscriptionSats = 546 + const calculatedFee = minFee * feeRate < 250 ? 250 : minFee * feeRate + let finalFee = calculatedFee + + const sortedUtxos = utxos.sort( + (a: { value: number }, b: { value: number }) => b.value - a.value + ) + + const amountRetrieved = calculateValueOfUtxosGathered(sortedUtxos) + + let psbt = new bitcoin.Psbt({ network: getBitcoinNetwork(network) }) + + for (let i = 0; i < sortedUtxos.length; i++) { + const script = bitcoin.address.toOutputScript(address, getBitcoinNetwork(network)) + const utxo = sortedUtxos[i] + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + value: BigInt(utxo.value), + script, + }, + }) + } + + const script = createRuneMintScript({ + runeId: runeId, + pointer: 1, + }) + + const output = { script: script, value: BigInt(0) } + psbt.addOutput(output) + + const changeAmount = + amountRetrieved - (finalFee + inscriptionSats) + + psbt.addOutput({ + value: BigInt(inscriptionSats), + address: address, + }) + + psbt.addOutput({ + address: address, + value: BigInt(changeAmount), + }) + + return { psbt: psbt.toBase64() } + } catch (error) { + console.log(error) + throw error + } +} diff --git a/packages/lasereyes-core/src/lib/runes/scripts.ts b/packages/lasereyes-core/src/lib/runes/scripts.ts new file mode 100644 index 0000000..7643d30 --- /dev/null +++ b/packages/lasereyes-core/src/lib/runes/scripts.ts @@ -0,0 +1,110 @@ +import { encodeRunestone, RunestoneSpec } from '@magiceden-oss/runestone-lib' +import { encodeVarint } from '../utils' + +export const createRuneMintScript = ({ + runeId, +}: { + runeId: string + pointer?: number +}) => { + const [blockStr, txStr] = runeId.split(':') + const runestone: RunestoneSpec = { + mint: { + block: BigInt(blockStr), + tx: parseInt(txStr, 10), + }, + } + return encodeRunestone(runestone).encodedRunestone +} + +export const createRuneEtchScript = ({ + pointer = 0, + runeName, + symbol, + divisibility, + perMintAmount, + premine = 0, + cap, + turbo, +}: { + pointer?: number + runeName: string + symbol: string + divisibility?: number + perMintAmount: number + cap?: number + premine?: number + turbo?: boolean +}) => { + const runeEtch = encodeRunestone({ + etching: { + divisibility, + premine: BigInt(premine), + runeName, + symbol, + terms: { + cap: cap ? BigInt(cap) : undefined, + amount: perMintAmount ? BigInt(perMintAmount) : undefined, + }, + turbo, + }, + pointer, + }).encodedRunestone + return runeEtch +} + +export const createRuneSendScript = ({ + runeId, + amount, + divisibility = 0, + sendOutputIndex = 1, + pointer = 0, +}: { + runeId: string + amount: number + divisibility?: number + sendOutputIndex?: number + pointer: number +}) => { + if (divisibility === 0) { + amount = Math.floor(amount) + } + const pointerFlag = encodeVarint(BigInt(22)).varint + const pointerVarint = encodeVarint(BigInt(pointer)).varint + const bodyFlag = encodeVarint(BigInt(0)).varint + const amountToSend = encodeVarint(BigInt(amount * 10 ** divisibility)).varint + const encodedOutputIndex = encodeVarint(BigInt(sendOutputIndex)).varint + const splitIdString = runeId.split(':') + const block = Number(splitIdString[0]) + const blockTx = Number(splitIdString[1]) + + const encodedBlock = encodeVarint(BigInt(block)).varint + const encodedBlockTxNumber = encodeVarint(BigInt(blockTx)).varint + + const runeStone = Buffer.concat([ + pointerFlag, + pointerVarint, + bodyFlag, + encodedBlock, + encodedBlockTxNumber, + amountToSend, + encodedOutputIndex, + ]) + + let runeStoneLength: string = runeStone.byteLength.toString(16) + + if (runeStoneLength.length % 2 !== 0) { + runeStoneLength = '0' + runeStone.byteLength.toString(16) + } + + const script: Buffer = Buffer.concat([ + Buffer.from('6a', 'hex'), + Buffer.from('5d', 'hex'), + Buffer.from(runeStoneLength, 'hex'), + runeStone, + ]) + return script +} + + + diff --git a/packages/lasereyes-core/src/lib/runes/utils.ts b/packages/lasereyes-core/src/lib/runes/utils.ts new file mode 100644 index 0000000..16260fd --- /dev/null +++ b/packages/lasereyes-core/src/lib/runes/utils.ts @@ -0,0 +1,25 @@ +import { SingleRuneOutpoint } from "../../types/sandshrew"; +import { batchOrdOutput, getOrdAddress, getRuneById, mapRuneBalances } from "../sandshrew"; + +export const getRuneOutpoints = async ({ + address, + runeId, +}: { + address: string + runeId: string +}): Promise => { + const addressOutpoints = await getOrdAddress(address); + const { entry } = await getRuneById(runeId); + const runeName = entry.spaced_rune; + + const ordOutputs = await batchOrdOutput({ + outpoints: addressOutpoints.outputs, + rune_name: runeName + }) + + const runeUtxosOutpoints = await mapRuneBalances({ + ordOutputs: ordOutputs, + }) + + return runeUtxosOutpoints; +} diff --git a/packages/lasereyes-core/src/lib/sandshrew.ts b/packages/lasereyes-core/src/lib/sandshrew.ts new file mode 100644 index 0000000..da8b2ff --- /dev/null +++ b/packages/lasereyes-core/src/lib/sandshrew.ts @@ -0,0 +1,154 @@ +import axios from "axios" +import { OrdAddress, OrdAddressResponse, OrdOutputs, OrdRune, RuneBalance } from "../types/ord" +import { EsploraTx, SingleRuneOutpoint } from "../types/sandshrew" +import { getPublicKeyHash } from "./btc" +import { MAINNET } from "../constants" +export const SANDSHREW_URL: string = "https://mainnet.sandshrew.io/v1/lasereyes" + +export const callSandshrewRPC = async (method: string, params: string | any) => { + const data = JSON.stringify({ + jsonrpc: '2.0', + id: method, + method: method, + params: params, + }) + + if (!SANDSHREW_URL) { + throw new Error('SANDSHREW_URL is not set') + } + + return await axios + .post(SANDSHREW_URL, data, { + headers: { + 'content-type': 'application/json', + }, + }) + .then((res) => res.data) + .catch((e) => { + throw e + }) +} + +export const getOrdAddress = async (address: string) => { + try { + const response = await callSandshrewRPC('ord_address', [address]) as OrdAddressResponse + return response.result as OrdAddress + } catch (e) { + throw e + } +} + +export const getRuneById = async (rune_id: string) => { + try { + const response = await callSandshrewRPC('ord_rune', [rune_id]) + return response.result as OrdRune + } catch (e) { + throw e + } +} + +export const getRuneByName = async (rune_name: string) => { + try { + const response = await callSandshrewRPC('ord_rune', [rune_name]) + return response + } catch (e) { + throw e + } +} + +export const getTxInfo = async (txId: string): Promise => { + try { + return await callSandshrewRPC('esplora_tx', [txId]) + } catch (e) { + console.error(e) + throw e + } +} + +export const batchOrdOutput = async ({ + outpoints, + rune_name +}: { + outpoints: string[] + rune_name: string +}): Promise => { + const MAX_OUTPOINTS_PER_CALL = 1000; + const ordOutputs: OrdOutputs[] = []; + for (let i = 0; i < outpoints.length; i += MAX_OUTPOINTS_PER_CALL) { + const batch = outpoints.slice(i, i + MAX_OUTPOINTS_PER_CALL); + const multiCall = batch.map((outpoint) => { + return ["ord_output", [outpoint]]; + }); + + const { result } = await callSandshrewRPC("sandshrew_multicall", multiCall); + for (let i = 0; i < result.length; i++) { + result[i].result["output"] = batch[i]; + } + + const filteredResult = result.filter((output: OrdOutputs) => Object.keys(output.result.runes).includes(rune_name)); + ordOutputs.push(...filteredResult); + } + return ordOutputs; +} + +export const getAddressRunesBalances = async (address: string) => { + try { + const response = await getOrdAddress(address) + const runesData = response.runes_balances; + if (!runesData) { + throw new Error('No runes data found') + } + + return runesData.map((rune: any) => ({ + name: rune[0], + balance: rune[1], + symbol: rune[2], + })) as RuneBalance[]; + } catch (error) { + console.error("Error fetching ord address:", error); + } +}; + +export const mapRuneBalances = async ({ + ordOutputs, +}: { + ordOutputs: OrdOutputs[] +}): Promise => { + try { + const runeOutpoints: SingleRuneOutpoint[] = []; + for (let i = 0; i < ordOutputs.length; i++) { + const ordOutput = ordOutputs[i]; + const { result } = ordOutput; + if (!result.output?.split(":")) { + throw new Error('No output found') + } + + const { output, address, runes } = result; + const singleRuneOutpoint: SingleRuneOutpoint = { + output, + wallet_addr: address, + script: "", + balances: [], + decimals: [], + rune_ids: [], + value: result.value, + }; + + const [txId, txIndex] = output.split(":"); + console.log(txId, txIndex, output); + singleRuneOutpoint["script"] = Buffer.from(getPublicKeyHash(address, MAINNET)).toString("hex"); + if (typeof runes === "object" && !Array.isArray(runes)) { + for (const rune in runes) { + singleRuneOutpoint.balances.push(runes[rune].amount); + singleRuneOutpoint.decimals.push(runes[rune].divisibility); + singleRuneOutpoint.rune_ids.push((await getRuneByName(rune)).id); + } + } + + runeOutpoints.push(singleRuneOutpoint); + } + return runeOutpoints; + } catch (e) { + throw e + } +} diff --git a/packages/lasereyes-core/src/lib/utils.ts b/packages/lasereyes-core/src/lib/utils.ts index d758794..b1588ec 100644 --- a/packages/lasereyes-core/src/lib/utils.ts +++ b/packages/lasereyes-core/src/lib/utils.ts @@ -8,3 +8,20 @@ export const isHex = (str: string): boolean => { const hexRegex = /^[a-fA-F0-9]+$/ return hexRegex.test(str) } + + +export const encodeVarint = (bigIntValue: any) => { + const bufferArray = [] + let num = bigIntValue + + do { + let byte = num & BigInt(0x7f) + num >>= BigInt(7) + if (num !== BigInt(0)) { + byte |= BigInt(0x80) + } + bufferArray.push(Number(byte)) + } while (num !== BigInt(0)) + + return { varint: Buffer.from(bufferArray) } +} diff --git a/packages/lasereyes-core/src/types/index.ts b/packages/lasereyes-core/src/types/index.ts index 0a8e4a0..4078c87 100644 --- a/packages/lasereyes-core/src/types/index.ts +++ b/packages/lasereyes-core/src/types/index.ts @@ -50,6 +50,7 @@ import { VIDEO_WEBM, TEXT_MARKDOWN, } from '../constants/content' +import { BTC, RUNES } from '../constants/protocols' export type NetworkType = | typeof MAINNET @@ -105,14 +106,34 @@ export type ContentType = export type Config = { network: - | typeof MAINNET - | typeof TESTNET - | typeof TESTNET4 - | typeof SIGNET - | typeof FRACTAL_MAINNET - | typeof FRACTAL_TESTNET + | typeof MAINNET + | typeof TESTNET + | typeof TESTNET4 + | typeof SIGNET + | typeof FRACTAL_MAINNET + | typeof FRACTAL_TESTNET } +export type SendArgs = BTCSendArgs | RuneSendArgs + +export type Protocol = typeof BTC | typeof RUNES + +export interface BTCSendArgs { + fromAddress: string + toAddress: string + amount: number + network: NetworkType +} + +export interface RuneSendArgs { + runeId: string + fromAddress: string + toAddress: string + amount: number + network: NetworkType +} + + export interface OYLBalanceResponse { brc20s: { total: number diff --git a/packages/lasereyes-core/src/types/ord.ts b/packages/lasereyes-core/src/types/ord.ts new file mode 100644 index 0000000..53a155e --- /dev/null +++ b/packages/lasereyes-core/src/types/ord.ts @@ -0,0 +1,65 @@ +export interface OrdOutputs { + result: OrdOutput; +} + +export type OrdOutputRune = { + amount: number + divisibility: number +} + +export interface OrdOutput { + address: string + indexed: boolean + inscriptions: string[] + runes: Record | OrdOutputRune[][] + sat_ranges: number[][] + script_pubkey: string + spent: boolean + transaction: string + value: number + output?: string +} + +export type RuneBalance = { + name: string; + balance: string; + symbol: string; +}; + +export type OrdAddressResponse = { + jsonrpc: string + id: number + result: OrdAddress +} + +export type OrdAddress = { + outputs: Array + inscriptions: Array + sat_balance: number + runes_balances: Array> +} + +export type OrdRune = { + entry: { + block: number + burned: number + divisibility: number + etching: string + mints: number + number: number + premine: number + spaced_rune: string + symbol: string + terms: { + amount: number + cap: number + height: Array + offset: Array + } + timestamp: number + turbo: boolean + } + id: string + mintable: boolean + parent: string +} diff --git a/packages/lasereyes-core/src/types/sandshrew.ts b/packages/lasereyes-core/src/types/sandshrew.ts new file mode 100644 index 0000000..8c68965 --- /dev/null +++ b/packages/lasereyes-core/src/types/sandshrew.ts @@ -0,0 +1,59 @@ +export interface SingleRuneOutpoint { + output: string; + wallet_addr: string; + balances: number[]; + decimals: number[]; + rune_ids: string[]; + script: string; + value: number; +}; + +export interface EsploraTx { + txid: string + version: number + locktime: number + vin: Array<{ + txid: string + vout: number + prevout: { + scriptpubkey: string + scriptpubkey_asm: string + scriptpubkey_type: string + scriptpubkey_address: string + value: number + } + scriptsig: string + scriptsig_asm: string + witness: Array + is_coinbase: boolean + sequence: number + }> + vout: Array<{ + scriptpubkey: string + scriptpubkey_asm: string + scriptpubkey_type: string + scriptpubkey_address: string + value: number + }> + size: number + weight: number + fee: number + status: { + confirmed: boolean + block_height: number + block_hash: string + block_time: number + } +} + +export interface EsploraUtxo { + txid: string + vout: number + status: { + confirmed: boolean + block_height?: number + block_hash?: string + block_time?: number + } + value: number +} diff --git a/packages/lasereyes-react/lib/providers/context.ts b/packages/lasereyes-react/lib/providers/context.ts index 2dd1c5a..d1be4c2 100644 --- a/packages/lasereyes-react/lib/providers/context.ts +++ b/packages/lasereyes-react/lib/providers/context.ts @@ -10,9 +10,10 @@ import { MapStore, WritableAtom } from 'nanostores' const { $store, $network } = createStores() export const defaultMethods = { - connect: async () => {}, - disconnect: () => {}, + connect: async () => { }, + disconnect: () => { }, getBalance: async () => '', + getMetaBalances: async () => [], getInscriptions: async () => [], getNetwork: async () => '', getPublicKey: async () => '', @@ -24,8 +25,9 @@ export const defaultMethods = { signedPsbtBase64: '', signedPsbtHex: '', }), - switchNetwork: async () => {}, + switchNetwork: async () => { }, inscribe: async () => '', + send: async () => '' } export const LaserEyesStoreContext = createContext<{ $store: MapStore @@ -39,11 +41,13 @@ export const LaserEyesStoreContext = createContext<{ | 'requestAccounts' | 'sendBTC' | 'inscribe' + | 'send' | 'getPublicKey' | 'getNetwork' | 'pushPsbt' | 'getInscriptions' | 'getBalance' + | 'getMetaBalances' | 'disconnect' | 'connect' > diff --git a/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx b/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx index 33b3e74..65a5ddf 100644 --- a/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx +++ b/packages/lasereyes-react/lib/providers/lasereyes-provider.tsx @@ -2,6 +2,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { defaultMethods, LaserEyesStoreContext } from './context' import { + BTCSendArgs, Config, ContentType, createConfig, @@ -9,7 +10,9 @@ import { LaserEyesClient, MAINNET, NetworkType, + Protocol, ProviderType, + RuneSendArgs, } from '@omnisat/lasereyes-core' export default function LaserEyesProvider({ @@ -45,6 +48,11 @@ export default function LaserEyesProvider({ )?.toString() ?? '', [client] ) + const getMetaBalances = useCallback( + async (protocol: Protocol) => + await client?.getMetaBalances(protocol) ?? defaultMethods.getMetaBalances(), + [client] + ) const getInscriptions = useCallback( async (offset?: number, limit?: number) => (await client?.getInscriptions(offset, limit)) ?? @@ -97,6 +105,12 @@ export default function LaserEyesProvider({ defaultMethods.inscribe(), [client] ) + const send = useCallback( + async (protocol: Protocol, sendArgs: BTCSendArgs | RuneSendArgs) => + (await client?.send.call(client, protocol, sendArgs)) ?? + defaultMethods.send(), + [client] + ) // TODO: Move method definitions into useMemo here const methods = useMemo(() => { @@ -108,6 +122,7 @@ export default function LaserEyesProvider({ connect, disconnect, getBalance, + getMetaBalances, getInscriptions, getNetwork, getPublicKey, @@ -118,6 +133,7 @@ export default function LaserEyesProvider({ signPsbt, switchNetwork, inscribe, + send } }, [ client, diff --git a/packages/lasereyes-react/lib/providers/types.ts b/packages/lasereyes-react/lib/providers/types.ts index 0acd6da..21c4ead 100644 --- a/packages/lasereyes-react/lib/providers/types.ts +++ b/packages/lasereyes-react/lib/providers/types.ts @@ -1,4 +1,4 @@ -import { ContentType, NetworkType, ProviderType } from '@omnisat/lasereyes-core' +import { ContentType, NetworkType, Protocol, ProviderType, SendArgs } from '@omnisat/lasereyes-core' export type LaserEyesContextType = { isInitializing: boolean @@ -32,6 +32,7 @@ export type LaserEyesContextType = { switchNetwork: (network: NetworkType) => Promise getPublicKey: () => Promise getBalance: () => Promise + getMetaBalances: (protocol: Protocol) => Promise getInscriptions: (offset?: number, limit?: number) => Promise sendBTC: (to: string, amount: number) => Promise signMessage: (message: string, toSignAddress?: string) => Promise @@ -41,10 +42,10 @@ export type LaserEyesContextType = { broadcast?: boolean ) => Promise< | { - signedPsbtHex: string | undefined - signedPsbtBase64: string | undefined - txId?: string - } + signedPsbtHex: string | undefined + signedPsbtBase64: string | undefined + txId?: string + } | undefined > pushPsbt: (tx: string) => Promise @@ -52,4 +53,5 @@ export type LaserEyesContextType = { contentBase64: string, mimeType: ContentType ) => Promise + send: (protocol: Protocol, sendArgs: SendArgs) => Promise } diff --git a/packages/lasereyes-react/package.json b/packages/lasereyes-react/package.json index 40c9a2d..00d43bd 100644 --- a/packages/lasereyes-react/package.json +++ b/packages/lasereyes-react/package.json @@ -1,7 +1,7 @@ { "name": "@omnisat/lasereyes-react", "private": false, - "version": "0.0.54", + "version": "0.0.55-rc.1", "type": "module", "main": "./dist/index.umd.cjs", "module": "./dist/index.js", diff --git a/packages/lasereyes/package.json b/packages/lasereyes/package.json index 08b9d4d..53cabf1 100644 --- a/packages/lasereyes/package.json +++ b/packages/lasereyes/package.json @@ -20,7 +20,7 @@ "url": "https://github.com/omnisat/lasereyes-mono.git" }, "private": false, - "version": "0.0.137", + "version": "0.0.138-rc.1", "type": "module", "main": "./dist/index.umd.cjs", "module": "./dist/index.js", @@ -56,4 +56,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83a10eb..efc20ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,6 +250,9 @@ importers: '@cmdcode/tapscript': specifier: ^1.4.6 version: 1.4.6 + '@magiceden-oss/runestone-lib': + specifier: ^1.0.2 + version: 1.0.2 '@nanostores/persistent': specifier: ^0.10.2 version: 0.10.2(nanostores@0.11.3) @@ -3278,6 +3281,10 @@ packages: resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} dev: false + /@magiceden-oss/runestone-lib@1.0.2: + resolution: {integrity: sha512-C4XoE7tHxxaWQNzovBIEndXGrTesotshnKQA8DXlnoVDvriD6G26ezUoj9AlEb2UAPbEijSga4Dtwd86Z0a1jg==} + dev: false + /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: