UNPKG

ecash-agora

Version:

Library for interacting with the eCash Agora protocol

1,214 lines (1,121 loc) 42.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, assert } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { ChronikClient } from 'chronik-client'; import { ALL_BIP143, ALP_STANDARD, ALP_TOKEN_TYPE_STANDARD, DEFAULT_DUST_SATS, Ecc, P2PKHSignatory, Script, TxBuilderInput, alpSend, emppScript, fromHex, payment, shaRmd160, toHex, } from 'ecash-lib'; import { TestRunner } from 'ecash-lib/dist/test/testRunner.js'; import { AgoraPartial } from '../src/partial.js'; import { makeAlpOffer, takeAlpOffer } from './partial-helper-alp.js'; import { Agora, TakenInfo } from '../src/agora.js'; import { Wallet } from 'ecash-wallet/src/wallet.js'; use(chaiAsPromised); // This test needs a lot of sats const NUM_COINS = 500; const COIN_VALUE = 1100000000n; const BASE_PARAMS_ALP = { tokenId: '00'.repeat(32), // filled in later tokenType: ALP_STANDARD, tokenProtocol: 'ALP' as const, dustSats: DEFAULT_DUST_SATS, }; 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); describe('AgoraPartial ALP', () => { let runner: TestRunner; let chronik: ChronikClient; async function makeTakerInputs(sats: bigint[]): Promise<void> { await runner.sendToScript(sats, takerScript); } async function makeBuilderInputs( 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), })); } before(async () => { runner = await TestRunner.setup('setup_scripts/ecash-agora_base'); chronik = runner.chronik; await runner.setupCoins(NUM_COINS, COIN_VALUE); }); after(() => { runner.stop(); }); interface TestCase { offeredAtoms: bigint; info: string; priceNanoSatsPerAtom: bigint; acceptedAtoms: bigint; askedSats: bigint; allowUnspendable?: boolean; } const TEST_CASES: TestCase[] = [ { offeredAtoms: 1000n, info: '1sat/token, full accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 1000n, askedSats: 1000n, }, { offeredAtoms: 1000n, info: '1sat/token, dust accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 546n, askedSats: 546n, allowUnspendable: true, }, { offeredAtoms: 1000n, info: '1000sat/token, full accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 1000n, askedSats: 1000225n, }, { offeredAtoms: 1000n, info: '1000sat/token, half accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 500n, askedSats: 500113n, }, { offeredAtoms: 1000n, info: '1000sat/token, 1 accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 1n, askedSats: 1001n, }, { offeredAtoms: 1000n, info: '1000000sat/token, full accept', priceNanoSatsPerAtom: 1000000n * 1000000000n, acceptedAtoms: 1000n, askedSats: 1000013824n, }, { offeredAtoms: 1000n, info: '1000000sat/token, half accept', priceNanoSatsPerAtom: 1000000n * 1000000000n, acceptedAtoms: 500n, askedSats: 500039680n, }, { offeredAtoms: 1000n, info: '1000000sat/token, 1 accept', priceNanoSatsPerAtom: 1000000n * 1000000000n, acceptedAtoms: 1n, askedSats: 1048576n, }, { offeredAtoms: 1000n, info: '1000000000sat/token, 1 accept', priceNanoSatsPerAtom: 1000000000n * 1000000000n, acceptedAtoms: 1n, askedSats: 1006632960n, }, { offeredAtoms: 1000000n, info: '0.001sat/token, full accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 1000000n, askedSats: 1000n, }, { offeredAtoms: 1000000n, info: '1sat/token, full accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 1000000n, askedSats: 1000000n, }, { offeredAtoms: 1000000n, info: '1sat/token, half accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 500000n, askedSats: 500000n, }, { offeredAtoms: 1000000n, info: '1sat/token, dust accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 546n, askedSats: 546n, }, { offeredAtoms: 1000000n, info: '1000sat/token, full accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 999936n, askedSats: 999948288n, }, { offeredAtoms: 1000000n, info: '1000sat/token, half accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 499968n, askedSats: 499974144n, }, { offeredAtoms: 1000000n, info: '1000sat/token, 256 accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 256n, askedSats: 262144n, }, { offeredAtoms: 1000000n, info: '1000000sat/token, 1024 accept', priceNanoSatsPerAtom: 1000000n * 1000000000n, acceptedAtoms: 1024n, askedSats: 1040187392n, }, { offeredAtoms: 1000000n, info: '1000000sat/token, 256 accept', priceNanoSatsPerAtom: 1000000n * 1000000000n, acceptedAtoms: 256n, askedSats: 268435456n, }, { offeredAtoms: 1000000000n, info: '0.001sat/token, full accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 1000000000n, askedSats: 1000000n, }, { offeredAtoms: 1000000000n, info: '0.001sat/token, half accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 500000000n, askedSats: 500000n, }, { offeredAtoms: 1000000000n, info: '0.001sat/token, dust accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 546000n, askedSats: 546n, }, { offeredAtoms: 1000000000n, info: '1sat/token, full accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 1000000000n, askedSats: 1000000000n, }, { offeredAtoms: 1000000000n, info: '1sat/token, half accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 500000000n, askedSats: 500000000n, }, { offeredAtoms: 1000000000n, info: '1sat/token, dust accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 546n, askedSats: 546n, }, { offeredAtoms: 1000000000n, info: '1000sat/token, 983040 accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 983040n, askedSats: 989855744n, }, { offeredAtoms: 1000000000n, info: '1000sat/token, 65536 accept', priceNanoSatsPerAtom: 1000n * 1000000000n, acceptedAtoms: 65536n, askedSats: 67108864n, }, { offeredAtoms: 1000000000000n, info: '0.000001sat/token, full accept', priceNanoSatsPerAtom: 1000n, acceptedAtoms: 999999995904n, askedSats: 1000108n, }, { offeredAtoms: 1000000000000n, info: '0.000001sat/token, half accept', priceNanoSatsPerAtom: 1000n, acceptedAtoms: 546045952n, askedSats: 547n, }, { offeredAtoms: 1000000000000n, info: '0.001sat/token, full accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 999999995904n, askedSats: 1068115230n, }, { offeredAtoms: 1000000000000n, info: '0.001sat/token, dust accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 589824n, askedSats: 630n, }, { offeredAtoms: 0x7fffffffffffn, info: '0.000000001sat/token, full accept', priceNanoSatsPerAtom: 1n, acceptedAtoms: 0x7fffc4660000n, askedSats: 140744n, }, { offeredAtoms: 0x7fffffffffffn, info: '0.000000001sat/token, dust accept', priceNanoSatsPerAtom: 1n, acceptedAtoms: 0x7f1e660000n, askedSats: 546n, }, { offeredAtoms: 0x7fffffffffffn, info: '0.000001sat/token, full accept', priceNanoSatsPerAtom: 1000n, acceptedAtoms: 0x7ffffff10000n, askedSats: 143165576n, }, { offeredAtoms: 0x7fffffffffffn, info: '0.000001sat/token, dust accept', priceNanoSatsPerAtom: 1000n, acceptedAtoms: 0x1ffd0000n, askedSats: 546n, }, { offeredAtoms: 0x7fffffffffffn, info: '0.001sat/token, max sats accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 799999983616n, askedSats: 1041666816n, }, { offeredAtoms: 0x7fffffffffffn, info: '0.001sat/token, dust accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 0x70000n, askedSats: 768n, }, { offeredAtoms: 0x7fffffffffffn, info: '1sat/token, max sats accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 999948288n, askedSats: 999948288n, }, { offeredAtoms: 0x7fffffffffffn, info: '1sat/token, min accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 0x10000n, askedSats: 0x10000n, }, { offeredAtoms: 0xffffffffffffn, info: '0.000000001sat/token, full accept', priceNanoSatsPerAtom: 1n, acceptedAtoms: 0xffffff000000n, askedSats: 281505n, }, { offeredAtoms: 0xffffffffffffn, info: '0.000000001sat/token, dust accept', priceNanoSatsPerAtom: 1n, acceptedAtoms: 0x7f1c000000n, askedSats: 546n, }, { offeredAtoms: 0xffffffffffffn, info: '0.000001sat/token, full accept', priceNanoSatsPerAtom: 1000n, acceptedAtoms: 0xffffff000000n, askedSats: 306783360n, }, { offeredAtoms: 0xffffffffffffn, info: '0.000001sat/token, dust accept', priceNanoSatsPerAtom: 1000n, acceptedAtoms: 0x1e000000n, askedSats: 549n, }, { offeredAtoms: 0xffffffffffffn, info: '0.001sat/token, max sats accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 0x8000000000n, askedSats: 1073741824n, }, { offeredAtoms: 0xffffffffffffn, info: '0.001sat/token, min accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 0x1000000n, askedSats: 32768n, }, { offeredAtoms: 0xffffffffffffn, info: '1sat/token, max sats accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 989855744n, askedSats: 989855744n, }, { offeredAtoms: 0xffffffffffffn, info: '1sat/token, min accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 0x1000000n, askedSats: 0x1000000n, }, ]; let cancelTxsMatchCount = 0; for (const testCase of TEST_CASES) { it(`AgoraPartial ALP ${testCase.offeredAtoms} for ${testCase.info}`, async () => { const agora = new Agora(chronik); const agoraPartial = await agora.selectParams( { offeredAtoms: testCase.offeredAtoms, priceNanoSatsPerAtom: testCase.priceNanoSatsPerAtom, minAcceptedAtoms: testCase.acceptedAtoms, makerPk, ...BASE_PARAMS_ALP, }, 32n, ); const askedSats = agoraPartial.askedSats(testCase.acceptedAtoms); const requiredSats = askedSats + 2000n; const [fuelInput] = await makeBuilderInputs([4000n, requiredSats]); await makeTakerInputs([10_000n, requiredSats]); const offer = await makeAlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const acceptTxid = await takeAlpOffer({ chronik, takerSk, offer, acceptedAtoms: testCase.acceptedAtoms, allowUnspendable: testCase.allowUnspendable, }); const acceptTx = await chronik.tx(acceptTxid); const offeredAtoms = agoraPartial.offeredAtoms(); const isFullAccept = testCase.acceptedAtoms == offeredAtoms; if (isFullAccept) { // FULL ACCEPT // 0th output is OP_RETURN eMPP AGR0 ad + ALP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( emppScript([ agoraPartial.adPushdata(), alpSend( 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(testCase.askedSats); expect(acceptTx.outputs[1].outputScript).to.equal( makerScriptHex, ); // 2nd output is tokens to taker expect(acceptTx.outputs[2].token?.atoms).to.equal(offeredAtoms); expect(acceptTx.outputs[2].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[2].outputScript).to.equal( takerScriptHex, ); // Offer is now gone const newOffers = await agora.activeOffersByTokenId( offer.token.tokenId, ); expect(newOffers).to.deep.equal([]); return; } // PARTIAL ACCEPT const leftoverTokens = offeredAtoms - testCase.acceptedAtoms; const leftovertruncAtoms = leftoverTokens >> BigInt(8 * agoraPartial.numAtomsTruncBytes); // 0th output is OP_RETURN eMPP AGR0 ad + ALP SEND expect(acceptTx.outputs[0].outputScript).to.equal( toHex( emppScript([ agoraPartial.adPushdata(), alpSend(agoraPartial.tokenId, agoraPartial.tokenType, [ 0n, leftoverTokens, testCase.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(testCase.askedSats); expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex); // 2nd output is back to the P2SH Script expect(acceptTx.outputs[2].token?.atoms).to.equal(leftoverTokens); 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( testCase.acceptedAtoms, ); expect(acceptTx.outputs[3].sats).to.equal(DEFAULT_DUST_SATS); expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex); // Offer is now modified const newOffers = await agora.activeOffersByTokenId( offer.token.tokenId, ); expect(newOffers.length).to.equal(1); const newOffer = newOffers[0]; expect(newOffer.variant).to.deep.equal({ type: 'PARTIAL', params: new AgoraPartial({ ...agoraPartial, truncAtoms: leftovertruncAtoms, }), }); // Cancel leftover offer const cancelFeeSats = newOffer.cancelFeeSats({ recipientScript: makerScript, extraInputs: [fuelInput], // dummy input for measuring }); const cancelTx = newOffer.cancelTx({ cancelSk: makerSk, fuelInputs: await makeBuilderInputs([cancelFeeSats]), recipientScript: makerScript, }); const cancelTxid = cancelTx.txid(); // Let's build and broadcast using cancel() instead const cancelWallet = Wallet.fromSk(makerSk, chronik); await cancelWallet.sync(); const cancelResult = await newOffer.cancel({ wallet: cancelWallet, }); const broadcastCancelTxid = cancelResult.broadcasted[0]; if (broadcastCancelTxid === cancelTxid) { cancelTxsMatchCount++; console.log( `${cancelTxsMatchCount} of ${TEST_CASES.length} produce equal txids with cancelTx() and cancel()`, ); // Between ~5 and ~8 of 46 of these txs are identical from each method // On inspection, when cancel() txid does not match, // it is because cancel has selected different fuel inputs // This is expected behavior, these txs still show change // going to the cancel wallet as expected } const cancelChronikTx = await chronik.tx(broadcastCancelTxid); expect(cancelChronikTx.outputs[1].token?.atoms).to.equal( leftoverTokens, ); expect(cancelChronikTx.outputs[1].outputScript).to.equal( makerScriptHex, ); // takerIndex is 2 for full accept, 3 for partial accept const takerIndex = isFullAccept ? 2 : 3; // Get takenInfo from offer creation params const takenInfo: TakenInfo = { sats: BigInt(testCase.askedSats), takerScriptHex: acceptTx.outputs[takerIndex].outputScript, atoms: testCase.acceptedAtoms, }; // Tx history by token ID const offers = [ { ...offer, status: 'TAKEN', takenInfo, }, { ...newOffer, status: 'CANCELED', }, ]; const actualOffers = await agora.historicOffers({ type: 'TOKEN_ID', tokenId: offer.token.tokenId, table: 'UNCONFIRMED', }); if (offers[0].status !== actualOffers.offers[0].status) { offers.reverse(); } expect(actualOffers).to.deep.equal({ offers, numTxs: 3, numPages: 1, }); }); } it('Without manually setting an over-ride, we are unable to accept an agora partial if the remaining offer would be unacceptable due to the terms of the contract', async () => { const thisTestCase: TestCase = { offeredAtoms: 1000n, info: '1sat/token, dust accept', priceNanoSatsPerAtom: 1000000000n, acceptedAtoms: 546n, askedSats: 546n, allowUnspendable: true, }; const agora = new Agora(chronik); const agoraPartial = await agora.selectParams( { offeredAtoms: thisTestCase.offeredAtoms, priceNanoSatsPerAtom: thisTestCase.priceNanoSatsPerAtom, minAcceptedAtoms: thisTestCase.acceptedAtoms, makerPk, ...BASE_PARAMS_ALP, }, 32n, ); const askedSats = agoraPartial.askedSats(thisTestCase.acceptedAtoms); const requiredSats = askedSats + 2000n; const [fuelInput] = await makeBuilderInputs([4000n, requiredSats]); const offer = await makeAlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const expectedError = `Accepting ${thisTestCase.acceptedAtoms} token satoshis would leave an amount lower than the min acceptable by the terms of this contract, and hence unacceptable. Accept fewer tokens or the full offer.`; // We can get the error from the isolated method expect(() => agoraPartial.preventUnacceptableRemainder( thisTestCase.acceptedAtoms, ), ).to.throw(Error, expectedError); // We get an error for test cases that would result in unspendable amounts // if we do not pass allowUnspendable to agoraOffer.acceptTx await assert.isRejected( takeAlpOffer({ chronik, takerSk, offer, acceptedAtoms: thisTestCase.acceptedAtoms, allowUnspendable: false, }), expectedError, ); // We can estimate the fee without this error, even though the offer is unacceptable expect( offer.acceptFeeSats({ recipientScript: offer.txBuilderInput.signData ?.redeemScript as Script, acceptedAtoms: thisTestCase.acceptedAtoms, }), ).to.equal(1725n); }); it('Without manually setting an over-ride, we are unable to accept an agora partial if the remaining offer would be unacceptable due to a price less than dust', async () => { // ecash-agora does not support creating an agora partial with min accept amount priced less than dust // from the approximateParams method // However we can still do this if we manually create a new AgoraPartial // I think it is okay to preserve this, as the protocol does technically allow it, // and perhaps a power user wants to do this for some reason // Manually build an offer equivalent to previous test but accepting 500 tokens const agoraPartial = new AgoraPartial({ truncAtoms: 1000n, numAtomsTruncBytes: 0, atomsScaleFactor: 2145336n, scaledTruncAtomsPerTruncSat: 2145336n, numSatsTruncBytes: 0, minAcceptedScaledTruncAtoms: 1072668000n, scriptLen: 209, enforcedLockTime: 1333546081, makerPk, ...BASE_PARAMS_ALP, }); const acceptedAtoms = 500n; const askedSats = agoraPartial.askedSats(acceptedAtoms); const requiredSats = askedSats + 2000n; const [fuelInput] = await makeBuilderInputs([4000n, requiredSats]); const offer = await makeAlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const expectedError = `Accepting 500 token satoshis would leave an amount priced lower than dust. Accept fewer tokens or the full offer.`; // We can get the error from the isolated method expect(() => agoraPartial.preventUnacceptableRemainder(acceptedAtoms), ).to.throw(Error, expectedError); // And from attempting to accept await assert.isRejected( takeAlpOffer({ chronik, takerSk, offer, acceptedAtoms: acceptedAtoms, allowUnspendable: false, }), expectedError, ); }); it('We can relist an ALP agora partial tx using the available relist() method', async () => { const testCase = { offeredAtoms: 0x7fffffffffffn, info: '0.001sat/token, max sats accept', priceNanoSatsPerAtom: 1000000n, acceptedAtoms: 799999983616n, askedSats: 1041666816n, }; // Chosen at random from the above vectors const agora = new Agora(chronik); const agoraPartial = await agora.selectParams( { offeredAtoms: testCase.offeredAtoms, priceNanoSatsPerAtom: testCase.priceNanoSatsPerAtom, minAcceptedAtoms: testCase.acceptedAtoms, makerPk, ...BASE_PARAMS_ALP, }, 32n, ); const [fuelInput] = await makeBuilderInputs([ 4000n, testCase.askedSats, ]); // Create an offer const offer = await makeAlpOffer({ chronik, agoraPartial, makerSk, fuelInput, }); const createdTokenId = offer.token.tokenId; // A valid offer is created with the expected price and offeredAtoms const priceThisOffer = offer.askedSats(testCase.acceptedAtoms); expect(priceThisOffer).to.equal(testCase.askedSats); // NB for 32-bit, the offered atoms are very unlikely to be what we asked for with selectParams const atomsThisOffer = 140737488158720n; expect(offer.token.atoms).to.equal(atomsThisOffer); // Relist it // First, create the partial we want to relist const relistParams = { // Compare 0x7fffffffffffn offeredAtoms: 0x00ffffffffffn, // Reduce by a factor of 100 priceNanoSatsPerAtom: 10000n, // Compare 799999983616n acceptedAtoms: 799999983716n, }; const relistPartial = await agora.selectParams( { offeredAtoms: relistParams.offeredAtoms, priceNanoSatsPerAtom: relistParams.priceNanoSatsPerAtom, // Unchanged minAcceptedAtoms: relistParams.acceptedAtoms, makerPk, ...BASE_PARAMS_ALP, tokenId: createdTokenId, }, 32n, ); // Create the relist wallet const relistWallet = Wallet.fromSk(makerSk, chronik); await relistWallet.sync(); // Relist our existing offer await offer.relist({ wallet: relistWallet, updatedPartial: relistPartial, }); // Query for this offer const activeOffers = await agora.activeOffersByTokenId( offer.token.tokenId, ); // We only have one active offer expect(activeOffers.length).to.equal(1); const relistedOffer = activeOffers[0]; // The price of the original acceptedAtoms is different expect(relistedOffer.askedSats(testCase.acceptedAtoms)).to.equal( 8032606n, ); // Not the same as the original offer price expect(relistedOffer.askedSats(testCase.acceptedAtoms)).not.to.equal( offer.askedSats(testCase.acceptedAtoms), ); // So is the listed quantity expect(relistedOffer.token.atoms).to.equal(1099511562240n); // vs prev 140737488158720n // Not the same as the original offer quantity expect(relistedOffer.token.atoms).not.to.equal(offer.token.atoms); }); it('Can list a partial ALP offer using list() method and take it with another wallet', async () => { const agora = new Agora(chronik); const makerSk = fromHex('aa'.repeat(32)); const makerWallet = Wallet.fromSk(makerSk, chronik); const makerPk = ecc.derivePubkey(makerSk); const makerPkh = shaRmd160(makerPk); const makerScript = Script.p2pkh(makerPkh); const takerSk = fromHex('bb'.repeat(32)); const takerWallet = Wallet.fromSk(takerSk, chronik); const takerPk = ecc.derivePubkey(takerSk); // Fund maker wallet await runner.sendToScript(50000n, makerScript); await makerWallet.sync(); // Create ALP token using Wallet const genesisAction: payment.Action = { outputs: [ { sats: 0n }, { sats: 546n, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, atoms: 1000000n, }, { sats: DEFAULT_DUST_SATS, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, isMintBaton: true, atoms: 0n, }, ], tokenActions: [ { type: 'GENESIS', tokenType: ALP_TOKEN_TYPE_STANDARD, genesisInfo: { tokenTicker: 'TEST ALP', decimals: 4, }, }, ], }; const genesisResult = await makerWallet .action(genesisAction) .build() .broadcast(); const tokenId = genesisResult.broadcasted[0]; // Wait for token to be indexed await makerWallet.sync(); // Create AgoraPartial offer using selectParams (64-bit) const agoraPartial = await agora.selectParams( { offeredAtoms: 100000n, priceNanoSatsPerAtom: 1_000_000_000n, // 1 XEC per token minAcceptedAtoms: 546n, // Cover dust makerPk, tokenId, tokenType: ALP_TOKEN_TYPE_STANDARD.number, tokenProtocol: 'ALP', }, 64n, ); // List the offer const listResult = await agoraPartial.list({ wallet: makerWallet, }); expect(listResult.success).to.equal(true); expect(listResult.broadcasted.length).to.equal(1); // Find the offer const offers = await agora.activeOffersByTokenId(tokenId); expect(offers.length).to.equal(1); const offer = offers[0]; // Fund taker wallet await runner.sendToScript(200000n, Script.p2pkh(shaRmd160(takerPk))); await takerWallet.sync(); // Take the offer using take() method const acceptedAtoms = 50000n; const takeResult = await offer.take({ wallet: takerWallet, covenantSk: takerSk, covenantPk: takerPk, acceptedAtoms, }); expect(takeResult.success).to.equal(true); expect(takeResult.broadcasted.length).to.equal(1); // Verify offer is still active (partial accept leaves remainder) const remainingOffers = await agora.activeOffersByTokenId(tokenId); expect(remainingOffers.length).to.equal(1); expect(remainingOffers[0].token.atoms).to.equal( 100000n - acceptedAtoms, ); }); it('Can list a partial ALP offer using list() method and cancel it with the same wallet', async () => { const agora = new Agora(chronik); const makerSk = fromHex('cc'.repeat(32)); const makerWallet = Wallet.fromSk(makerSk, chronik); const makerPk = ecc.derivePubkey(makerSk); const makerPkh = shaRmd160(makerPk); const makerScript = Script.p2pkh(makerPkh); // Fund maker wallet await runner.sendToScript(50000n, makerScript); await makerWallet.sync(); // Create ALP token using Wallet const genesisAction: payment.Action = { outputs: [ { sats: 0n }, { sats: 546n, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, atoms: 1000000n, }, { sats: DEFAULT_DUST_SATS, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, isMintBaton: true, atoms: 0n, }, ], tokenActions: [ { type: 'GENESIS', tokenType: ALP_TOKEN_TYPE_STANDARD, genesisInfo: { tokenTicker: 'TEST ALP 2', decimals: 4, }, }, ], }; const genesisResult = await makerWallet .action(genesisAction) .build() .broadcast(); const tokenId = genesisResult.broadcasted[0]; // Wait for token to be indexed await makerWallet.sync(); // Create AgoraPartial offer using selectParams (64-bit) const agoraPartial = await agora.selectParams( { offeredAtoms: 100000n, priceNanoSatsPerAtom: 1_000_000_000n, // 1 XEC per token minAcceptedAtoms: 546n, // Cover dust makerPk, tokenId, tokenType: ALP_TOKEN_TYPE_STANDARD.number, tokenProtocol: 'ALP', }, 64n, ); // List the offer const listResult = await agoraPartial.list({ wallet: makerWallet, }); expect(listResult.success).to.equal(true); expect(listResult.broadcasted.length).to.equal(1); // Find the offer const offers = await agora.activeOffersByTokenId(tokenId); expect(offers.length).to.equal(1); const offer = offers[0]; // Fund maker wallet for cancel fee await runner.sendToScript(50000n, makerScript); await makerWallet.sync(); // Cancel the offer using cancel() method const cancelResult = await offer.cancel({ wallet: makerWallet, }); expect(cancelResult.success).to.equal(true); expect(cancelResult.broadcasted.length).to.equal(1); // Verify offer is no longer active const remainingOffers = await agora.activeOffersByTokenId(tokenId); expect(remainingOffers.length).to.equal(0); }); it('Can list() a partial ALP offer and cancel() with the same wallet, WITHOUT syncing in between', async () => { const agora = new Agora(chronik); const makerSk = fromHex('f4'.repeat(32)); const makerWallet = Wallet.fromSk(makerSk, chronik); const makerPk = ecc.derivePubkey(makerSk); const makerPkh = shaRmd160(makerPk); const makerScript = Script.p2pkh(makerPkh); await runner.sendToScript(400000n, makerScript); await makerWallet.sync(); const genesisAction: payment.Action = { outputs: [ { sats: 0n }, { sats: 546n, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, atoms: 1000000n, }, { sats: DEFAULT_DUST_SATS, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, isMintBaton: true, atoms: 0n, }, ], tokenActions: [ { type: 'GENESIS', tokenType: ALP_TOKEN_TYPE_STANDARD, genesisInfo: { tokenTicker: 'ALP NOSYNC C', decimals: 4, }, }, ], }; const genesisResult = await makerWallet .action(genesisAction) .build() .broadcast(); const genesisBr = genesisResult.broadcasted; const tokenId = genesisBr[genesisBr.length - 1]; await makerWallet.sync(); const agoraPartial = await agora.selectParams( { offeredAtoms: 100000n, priceNanoSatsPerAtom: 1_000_000_000n, minAcceptedAtoms: 546n, makerPk, tokenId, tokenType: ALP_TOKEN_TYPE_STANDARD.number, tokenProtocol: 'ALP', }, 64n, ); const listResult = await agoraPartial.list({ wallet: makerWallet, }); expect(listResult.success).to.equal(true); expect(listResult.broadcasted.length).to.equal(1); const offers = await agora.activeOffersByTokenId(tokenId); expect(offers.length).to.equal(1); const offer = offers[0]; const cancelResult = await offer.cancel({ wallet: makerWallet, }); expect(cancelResult.success).to.equal(true); expect(cancelResult.broadcasted.length).to.equal(1); expect((await agora.activeOffersByTokenId(tokenId)).length).to.equal(0); }); it('Can list() a partial ALP offer and relist() with the same wallet, WITHOUT syncing in between', async () => { const agora = new Agora(chronik); const makerSk = fromHex('f5'.repeat(32)); const makerWallet = Wallet.fromSk(makerSk, chronik); const makerPk = ecc.derivePubkey(makerSk); const makerPkh = shaRmd160(makerPk); const makerScript = Script.p2pkh(makerPkh); await runner.sendToScript(450000n, makerScript); await makerWallet.sync(); const genesisAction: payment.Action = { outputs: [ { sats: 0n }, { sats: 546n, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, atoms: 1000000n, }, { sats: DEFAULT_DUST_SATS, tokenId: payment.GENESIS_TOKEN_ID_PLACEHOLDER, script: makerWallet.script, isMintBaton: true, atoms: 0n, }, ], tokenActions: [ { type: 'GENESIS', tokenType: ALP_TOKEN_TYPE_STANDARD, genesisInfo: { tokenTicker: 'ALP NOSYNC R', decimals: 4, }, }, ], }; const genesisResult = await makerWallet .action(genesisAction) .build() .broadcast(); const genesisBr = genesisResult.broadcasted; const tokenId = genesisBr[genesisBr.length - 1]; await makerWallet.sync(); const agoraPartialInitial = await agora.selectParams( { offeredAtoms: 100000n, priceNanoSatsPerAtom: 1_000_000_000n, minAcceptedAtoms: 546n, makerPk, tokenId, tokenType: ALP_TOKEN_TYPE_STANDARD.number, tokenProtocol: 'ALP', }, 64n, ); const listResult = await agoraPartialInitial.list({ wallet: makerWallet, }); expect(listResult.success).to.equal(true); const offers = await agora.activeOffersByTokenId(tokenId); expect(offers.length).to.equal(1); const offer = offers[0]; const updatedPartial = await agora.selectParams( { offeredAtoms: 80000n, priceNanoSatsPerAtom: 2_000_000_000n, minAcceptedAtoms: 546n, makerPk, tokenId, tokenType: ALP_TOKEN_TYPE_STANDARD.number, tokenProtocol: 'ALP', }, 64n, ); const relistResult = await offer.relist({ wallet: makerWallet, updatedPartial, }); expect(relistResult.success).to.equal(true); expect((await agora.activeOffersByTokenId(tokenId)).length).to.equal(1); }); });