UNPKG

ecash-agora

Version:

Library for interacting with the eCash Agora protocol

636 lines (581 loc) 22.8 kB
// Copyright (c) 2024 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ChronikClient } from 'chronik-client'; import { ALL_BIP143, DEFAULT_DUST_SATS, Ecc, P2PKHSignatory, SLP_FUNGIBLE, Script, TxBuilderInput, fromHex, shaRmd160, slpSend, toHex, } from 'ecash-lib'; import { TestRunner } from 'ecash-lib/dist/test/testRunner.js'; import { AgoraPartial } from '../src/partial.js'; import { makeSlpOffer, takeSlpOffer } from './partial-helper-slp.js'; import { Agora } from '../src/agora.js'; use(chaiAsPromised); const BASE_PARAMS_SLP = { tokenId: '00'.repeat(32), // filled in later tokenType: SLP_FUNGIBLE, tokenProtocol: 'SLP' as const, dustSats: DEFAULT_DUST_SATS, }; const BIGSATS = BigInt(149 * 5000000000 - 20000); const ecc = new Ecc(); const makerSk = fromHex('33'.repeat(32)); const makerPk = ecc.derivePubkey(makerSk); const makerPkh = shaRmd160(makerPk); const makerScript = Script.p2pkh(makerPkh); const makerScriptHex = toHex(makerScript.bytecode); const takerSk = fromHex('44'.repeat(32)); const takerPk = ecc.derivePubkey(takerSk); const takerPkh = shaRmd160(takerPk); const takerScript = Script.p2pkh(takerPkh); const takerScriptHex = toHex(takerScript.bytecode); async function makeBuilderInputs( runner: TestRunner, values: bigint[], ): Promise<TxBuilderInput[]> { const txid = await runner.sendToScript(values, makerScript); return values.map((sats, outIdx) => ({ input: { prevOut: { txid, outIdx, }, signData: { sats, outputScript: makerScript, }, }, signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143), })); } describe('Agora Partial 7450M XEC vs 2p64-1 full accept', () => { let runner: TestRunner; let chronik: ChronikClient; before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(1, BIGSATS + 11000n); }); after(() => { runner.stop(); }); it('Agora Partial 7450M XEC vs 2p64-1 full accept', async () => { const [fuelInput, takerInput] = await makeBuilderInputs(runner, [ 10000n, BIGSATS, ]); const agora = new Agora(chronik); const agoraPartial = await agora.selectParams({ offeredAtoms: 0xffffffffffffffffn, priceNanoSatsPerAtom: 40n, // scaled to use the XEC makerPk: makerPk, minAcceptedAtoms: 0xffffffffffffn, ...BASE_PARAMS_SLP, }); expect(agoraPartial).to.deep.equal( new AgoraPartial({ truncAtoms: 0xffffffn, numAtomsTruncBytes: 5, atomsScaleFactor: 127n, scaledTruncAtomsPerTruncSat: 189n, numSatsTruncBytes: 2, makerPk, minAcceptedScaledTruncAtoms: 32511n, ...BASE_PARAMS_SLP, enforcedLockTime: agoraPartial.enforcedLockTime, scriptLen: 224, }), ); expect(agoraPartial.offeredAtoms()).to.equal(0xffffff0000000000n); expect(agoraPartial.askedSats(0x10000000000n)).to.equal(65536n); expect(agoraPartial.priceNanoSatsPerAtom(0x10000000000n)).to.equal(59n); expect(agoraPartial.askedSats(0xffffff0000000000n)).to.equal( 738825273344n, ); expect(agoraPartial.priceNanoSatsPerAtom(0xffffff0000000000n)).to.equal( 40n, ); expect(agoraPartial.priceNanoSatsPerAtom()).to.equal(40n); const offer = await makeSlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptTxid = await takeSlpOffer({ chronik, offer, takerSk, takerInput, acceptedAtoms: agoraPartial.offeredAtoms(), }); const acceptTx = await chronik.tx(acceptTxid); // 0th output is OP_RETURN SLP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, agoraPartial.offeredAtoms(), ]).bytecode, ), ); expect(acceptTx.outputs[0].sats).to.equal(0n); expect(acceptTx.outputs[0].token).to.equal(undefined); // 1st output is sats to maker expect(acceptTx.outputs[1].token).to.equal(undefined); expect(acceptTx.outputs[1].sats).to.equal(738825273344n); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is tokens to taker expect(acceptTx.outputs[2].token?.atoms).to.equal(0xffffff0000000000n); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript).to.equal(takerScriptHex); }); }); describe('Agora Partial 7450M XEC vs 2p64-1 small accept', () => { let runner: TestRunner; let chronik: ChronikClient; before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(1, BIGSATS + 11000n); }); after(() => { runner.stop(); }); it('Agora Partial 7450M XEC vs 2p64-1 small accept', async () => { const [fuelInput, takerInput] = await makeBuilderInputs(runner, [ 10000n, BIGSATS, ]); const agora = new Agora(chronik); const agoraPartial = await agora.selectParams({ offeredAtoms: 0xffffffffffffffffn, priceNanoSatsPerAtom: 500000000n, // scaled to use the XEC makerPk, minAcceptedAtoms: 0xffffffffffn, ...BASE_PARAMS_SLP, }); expect(agoraPartial).to.deep.equal( new AgoraPartial({ truncAtoms: 0xffffffn, numAtomsTruncBytes: 5, atomsScaleFactor: 128n, scaledTruncAtomsPerTruncSat: 1n, numSatsTruncBytes: 4, makerPk, minAcceptedScaledTruncAtoms: 0x7fn, ...BASE_PARAMS_SLP, enforcedLockTime: agoraPartial.enforcedLockTime, scriptLen: 224, }), ); expect(agoraPartial.offeredAtoms()).to.equal(0xffffff0000000000n); expect(agoraPartial.askedSats(0x10000000000n)).to.equal(549755813888n); expect(agoraPartial.priceNanoSatsPerAtom(0x10000000000n)).to.equal( 500000000n, ); expect(agoraPartial.askedSats(0xffffff0000000000n)).to.equal( 9223371487098961920n, ); expect(agoraPartial.priceNanoSatsPerAtom(0xffffff0000000000n)).to.equal( 500000000n, ); expect(agoraPartial.priceNanoSatsPerAtom()).to.equal(500000000n); const offer = await makeSlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptedAtoms = 0x10000000000n; const acceptTxid = await takeSlpOffer({ chronik, offer, takerSk, takerInput, acceptedAtoms, }); const acceptTx = await chronik.tx(acceptTxid); // 0th output is OP_RETURN SLP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, agoraPartial.offeredAtoms() - acceptedAtoms, acceptedAtoms, ]).bytecode, ), ); expect(acceptTx.outputs[0].sats).to.equal(0n); expect(acceptTx.outputs[0].token).to.equal(undefined); // 1st output is sats to maker expect(acceptTx.outputs[1].token).to.equal(undefined); expect(acceptTx.outputs[1].sats).to.equal(549755813888n); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is back to the P2SH Script expect(acceptTx.outputs[2].token?.atoms).to.equal( agoraPartial.offeredAtoms() - acceptedAtoms, ); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal('a914'); // 3rd output is tokens to taker expect(acceptTx.outputs[3].token?.atoms).to.equal(acceptedAtoms); expect(acceptTx.outputs[3].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex); }); }); describe('Agora Partial 7450M XEC vs 2p63-1 full accept', () => { let runner: TestRunner; let chronik: ChronikClient; before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(1, BIGSATS + 11000n); }); after(() => { runner.stop(); }); it('Agora Partial 7450M XEC vs 2p63-1 full accept', async () => { const [fuelInput, takerInput] = await makeBuilderInputs(runner, [ 10000n, BIGSATS, ]); const agora = new Agora(chronik); const agoraPartial = await agora.selectParams({ offeredAtoms: 0x7fffffffffffffffn, priceNanoSatsPerAtom: 80n, // scaled to use the XEC makerPk: makerPk, minAcceptedAtoms: 0xffffffffffffn, ...BASE_PARAMS_SLP, }); expect(agoraPartial).to.deep.equal( new AgoraPartial({ truncAtoms: 0x7fffff42n, numAtomsTruncBytes: 4, atomsScaleFactor: 1n, scaledTruncAtomsPerTruncSat: 190n, numSatsTruncBytes: 2, makerPk, minAcceptedScaledTruncAtoms: 0xffffn, ...BASE_PARAMS_SLP, enforcedLockTime: agoraPartial.enforcedLockTime, scriptLen: 214, }), ); expect(agoraPartial.offeredAtoms()).to.equal(0x7fffff4200000000n); expect(agoraPartial.askedSats(0x100000000n)).to.equal(65536n); expect(agoraPartial.priceNanoSatsPerAtom(0x100000000n)).to.equal( 15258n, ); expect(agoraPartial.askedSats(0x7fffff4200000000n)).to.equal( 740723589120n, ); expect(agoraPartial.priceNanoSatsPerAtom(0x7fffff4200000000n)).to.equal( 80n, ); expect(agoraPartial.priceNanoSatsPerAtom()).to.equal(80n); const offer = await makeSlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptedAtoms = agoraPartial.offeredAtoms(); const acceptTxid = await takeSlpOffer({ chronik, offer, takerSk, takerInput, acceptedAtoms, }); const acceptTx = await chronik.tx(acceptTxid); // 0th output is OP_RETURN SLP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, acceptedAtoms, ]).bytecode, ), ); expect(acceptTx.outputs[0].sats).to.equal(0n); expect(acceptTx.outputs[0].token).to.equal(undefined); // 1st output is sats to maker expect(acceptTx.outputs[1].token).to.equal(undefined); expect(acceptTx.outputs[1].sats).to.equal(740723589120n); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is tokens to taker expect(acceptTx.outputs[2].token?.atoms).to.equal(acceptedAtoms); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript).to.equal(takerScriptHex); }); }); describe('Agora Partial 7450M XEC vs 2p63-1 small accept', () => { let runner: TestRunner; let chronik: ChronikClient; before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(1, BIGSATS + 11000n); }); after(() => { runner.stop(); }); it('Agora Partial 7450M XEC vs 2p63-1 small accept', async () => { const [fuelInput, takerInput] = await makeBuilderInputs(runner, [ 10000n, BIGSATS, ]); const agora = new Agora(chronik); const agoraPartial = await agora.selectParams({ offeredAtoms: 0x7fffffffffffffffn, priceNanoSatsPerAtom: 1000000000n, makerPk, minAcceptedAtoms: 0x100000000n, ...BASE_PARAMS_SLP, }); expect(agoraPartial).to.deep.equal( new AgoraPartial({ truncAtoms: 0x7fffffffn, numAtomsTruncBytes: 4, atomsScaleFactor: 1n, scaledTruncAtomsPerTruncSat: 1n, numSatsTruncBytes: 4, makerPk, minAcceptedScaledTruncAtoms: 1n, ...BASE_PARAMS_SLP, enforcedLockTime: agoraPartial.enforcedLockTime, scriptLen: 209, }), ); expect(agoraPartial.offeredAtoms()).to.equal(0x7fffffff00000000n); expect(agoraPartial.askedSats(0x100000000n)).to.equal(4294967296n); expect(agoraPartial.priceNanoSatsPerAtom(0x100000000n)).to.equal( 1000000000n, ); expect(agoraPartial.askedSats(0x7fffffff00000000n)).to.equal( 9223372032559808512n, ); expect(agoraPartial.priceNanoSatsPerAtom(0x7fffffff00000000n)).to.equal( 1000000000n, ); expect(agoraPartial.priceNanoSatsPerAtom()).to.equal(1000000000n); const offer = await makeSlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptedAtoms = 0x100000000n; const acceptTxid = await takeSlpOffer({ chronik, offer, takerSk, takerInput, acceptedAtoms, }); const acceptTx = await chronik.tx(acceptTxid); // 0th output is OP_RETURN SLP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, agoraPartial.offeredAtoms() - acceptedAtoms, acceptedAtoms, ]).bytecode, ), ); expect(acceptTx.outputs[0].sats).to.equal(0n); expect(acceptTx.outputs[0].token).to.equal(undefined); // 1st output is sats to maker expect(acceptTx.outputs[1].token).to.equal(undefined); expect(acceptTx.outputs[1].sats).to.equal(4294967296n); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is back to the P2SH Script expect(acceptTx.outputs[2].token?.atoms).to.equal( agoraPartial.offeredAtoms() - acceptedAtoms, ); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal('a914'); // 3rd output is tokens to taker expect(acceptTx.outputs[3].token?.atoms).to.equal(acceptedAtoms); expect(acceptTx.outputs[3].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex); }); }); describe('Agora Partial 7450M XEC vs 100 full accept', () => { let runner: TestRunner; let chronik: ChronikClient; before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(1, BIGSATS + 11000n); }); after(() => { runner.stop(); }); it('Agora Partial 7450M XEC vs 100 full accept', async () => { const [fuelInput, takerInput] = await makeBuilderInputs(runner, [ 10000n, BIGSATS, ]); const agora = new Agora(chronik); const agoraPartial = await agora.selectParams({ offeredAtoms: 100n, priceNanoSatsPerAtom: 7123456780n * 1000000000n, // scaled to use the XEC makerPk: makerPk, minAcceptedAtoms: 1n, ...BASE_PARAMS_SLP, }); expect(agoraPartial).to.deep.equal( new AgoraPartial({ truncAtoms: 100n, numAtomsTruncBytes: 0, atomsScaleFactor: 0x7fff3a28n / 100n, scaledTruncAtomsPerTruncSat: 50576n, numSatsTruncBytes: 3, makerPk, minAcceptedScaledTruncAtoms: 0x7fff3a28n / 100n, ...BASE_PARAMS_SLP, enforcedLockTime: agoraPartial.enforcedLockTime, scriptLen: 222, }), ); expect(agoraPartial.offeredAtoms()).to.equal(100n); expect(agoraPartial.minAcceptedAtoms()).to.equal(1n); expect(agoraPartial.askedSats(1n)).to.equal(7130316800n); expect(agoraPartial.askedSats(2n)).to.equal(7130316800n * 2n); expect(agoraPartial.askedSats(3n)).to.equal(7124724394n * 3n + 2n); expect(agoraPartial.askedSats(4n)).to.equal(7126122496n * 4n); expect(agoraPartial.askedSats(5n)).to.equal(71236059136n / 2n); expect(agoraPartial.askedSats(10n)).to.equal(71236059136n); expect(agoraPartial.askedSats(100n)).to.equal(712360591360n); const offer = await makeSlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptTxid = await takeSlpOffer({ chronik, offer, takerSk, takerInput, acceptedAtoms: 100n, }); const acceptTx = await chronik.tx(acceptTxid); // 0th output is OP_RETURN SLP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, 100n, ]).bytecode, ), ); expect(acceptTx.outputs[0].sats).to.equal(0n); expect(acceptTx.outputs[0].token).to.equal(undefined); // 1st output is sats to maker expect(acceptTx.outputs[1].token).to.equal(undefined); expect(acceptTx.outputs[1].sats).to.equal(712360591360n); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is tokens to taker expect(acceptTx.outputs[2].token?.atoms).to.equal(100n); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript).to.equal(takerScriptHex); }); }); describe('Agora Partial 7450M XEC vs 100 small accept', () => { let runner: TestRunner; let chronik: ChronikClient; before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(1, BIGSATS + 11000n); }); after(() => { runner.stop(); }); it('Agora Partial 7450M XEC vs 100 small accept', async () => { const [fuelInput, takerInput] = await makeBuilderInputs(runner, [ 10000n, BIGSATS, ]); const agora = new Agora(chronik); const agoraPartial = await agora.selectParams({ offeredAtoms: 100n, priceNanoSatsPerAtom: 712345678000n * 1000000000n, // scaled to use the XEC makerPk: makerPk, minAcceptedAtoms: 1n, ...BASE_PARAMS_SLP, }); expect(agoraPartial).to.deep.equal( new AgoraPartial({ truncAtoms: 100n, numAtomsTruncBytes: 0, atomsScaleFactor: 0x7ffe05f4n / 100n, scaledTruncAtomsPerTruncSat: 129471n, numSatsTruncBytes: 4, makerPk, minAcceptedScaledTruncAtoms: 0x7ffe05f4n / 100n, ...BASE_PARAMS_SLP, enforcedLockTime: agoraPartial.enforcedLockTime, scriptLen: 223, }), ); expect(agoraPartial.offeredAtoms()).to.equal(100n); expect(agoraPartial.minAcceptedAtoms()).to.equal(1n); expect(agoraPartial.askedSats(1n)).to.equal(712964571136n); expect(agoraPartial.askedSats(10n)).to.equal(7125350744064n); expect(agoraPartial.askedSats(100n)).to.equal(71236327571456n); const offer = await makeSlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptTxid = await takeSlpOffer({ chronik, offer, takerSk, takerInput, acceptedAtoms: 1n, }); const acceptTx = await chronik.tx(acceptTxid); // 0th output is OP_RETURN SLP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, 99n, 1n, ]).bytecode, ), ); expect(acceptTx.outputs[0].sats).to.equal(0n); expect(acceptTx.outputs[0].token).to.equal(undefined); // 1st output is sats to maker expect(acceptTx.outputs[1].token).to.equal(undefined); expect(acceptTx.outputs[1].sats).to.equal(712964571136n); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is back to the P2SH Script expect(acceptTx.outputs[2].token?.atoms).to.equal(99n); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal('a914'); // 3rd output is tokens to taker expect(acceptTx.outputs[3].token?.atoms).to.equal(1n); expect(acceptTx.outputs[3].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex); }); });