Page MenuHomePhabricator

D16745.diff
No OneTemporary

D16745.diff

diff --git a/modules/ecash-agora/agora.py b/modules/ecash-agora/agora.py
--- a/modules/ecash-agora/agora.py
+++ b/modules/ecash-agora/agora.py
@@ -13,32 +13,57 @@
import hashlib
from dataclasses import dataclass
-from typing import Optional
+from io import BytesIO
+from typing import Optional, Union
from chronik_plugin.plugin import Plugin, PluginOutput
from chronik_plugin.script import (
+ OP_0,
+ OP_0NOTEQUAL,
OP_2,
+ OP_2DUP,
+ OP_2OVER,
+ OP_2SWAP,
OP_3DUP,
+ OP_8,
+ OP_9,
+ OP_12,
+ OP_ADD,
+ OP_BIN2NUM,
OP_CAT,
OP_CHECKDATASIGVERIFY,
OP_CHECKSIG,
OP_CHECKSIGVERIFY,
OP_CODESEPARATOR,
+ OP_DIV,
OP_DROP,
+ OP_DUP,
OP_ELSE,
OP_ENDIF,
OP_EQUAL,
OP_EQUALVERIFY,
+ OP_FROMALTSTACK,
+ OP_GREATERTHANOREQUAL,
OP_HASH160,
OP_HASH256,
OP_IF,
+ OP_MOD,
OP_NIP,
+ OP_NOTIF,
OP_NUM2BIN,
OP_OVER,
+ OP_PICK,
+ OP_PUSHDATA1,
+ OP_REVERSEBYTES,
OP_ROT,
OP_SHA256,
+ OP_SIZE,
OP_SPLIT,
+ OP_SUB,
OP_SWAP,
+ OP_TOALTSTACK,
+ OP_TUCK,
+ OP_VERIFY,
CScript,
)
from chronik_plugin.slp import slp_send
@@ -79,16 +104,7 @@
return []
ad_input = tx.inputs[0]
- offer_output = tx.outputs[1]
- token = offer_output.token
-
- # Offer must have a token
- if token is None:
- return []
-
token_entry = tx.token_entries[0]
- if token.token_id != token_entry.token_id:
- return []
pushdata = parse_ad_script_sig(ad_input.script)
if pushdata is None:
@@ -98,9 +114,15 @@
ad_redeem_script = CScript(ad_redeem_bytecode)
if covenant_variant == b"ONESHOT":
+ # Offer output is always output 1
+ offer_idx = 1
+ offer_output = tx.outputs[offer_idx]
+ # Offer must have a token
+ if offer_output.token is None:
+ return []
agora_oneshot = AgoraOneshot.parse_redeem_script(
ad_redeem_script,
- token,
+ offer_output.token,
)
if agora_oneshot is None:
return []
@@ -121,10 +143,45 @@
]
data = agora_oneshot.data()
pubkey = agora_oneshot.cancel_pk
+ elif covenant_variant == b"PARTIAL":
+ # Offer output is either output 1 or 2
+ offer_idx = 1
+ offer_output = tx.outputs[offer_idx]
+ if len(tx.outputs) >= 3 and offer_output.token is None:
+ offer_idx = 2
+ offer_output = tx.outputs[offer_idx]
+ # Offer must have a token
+ if offer_output.token is None:
+ return []
+
+ agora_partial = AgoraPartial.parse_redeem_script(
+ ad_redeem_script, offer_output.token
+ )
+ if agora_partial is None:
+ return []
+
+ expected_agora_script = agora_partial.script()
+ expected_agora_sh = hash160(expected_agora_script)
+ expected_agora_p2sh = CScript(
+ bytes([OP_HASH160, 20]) + expected_agora_sh + bytes([OP_EQUAL])
+ )
+
+ if offer_output.script != expected_agora_p2sh:
+ # Offered output doesn't have the advertized P2SH script
+ return [
+ PluginOutput(
+ idx=offer_idx,
+ data=[b"ERROR", expected_agora_script],
+ groups=[],
+ )
+ ]
+
+ data = agora_partial.data()
+ pubkey = agora_partial.maker_pk
else:
return []
- token_id_bytes = bytes.fromhex(token.token_id)
+ token_id_bytes = bytes.fromhex(token_entry.token_id)
groups = [
b"P" + pubkey,
b"T" + token_id_bytes,
@@ -136,7 +193,7 @@
return [
PluginOutput(
- idx=1,
+ idx=offer_idx,
data=data,
groups=groups,
)
@@ -146,10 +203,10 @@
MIN_NUM_SCRIPTSIG_PUSHOPS = 3
-def parse_ad_script_sig(script) -> Optional[list[bytes]]:
+def parse_ad_script_sig(script) -> Optional[list[Union[bytes, int]]]:
pushdata = []
for op in script:
- if not isinstance(op, bytes):
+ if not isinstance(op, (bytes, int)):
return None
pushdata.append(op)
if len(pushdata) < MIN_NUM_SCRIPTSIG_PUSHOPS:
@@ -269,3 +326,322 @@
OP_CHECKSIG,
]
)
+
+
+@dataclass
+class AgoraPartial:
+ trunc_tokens: int
+ num_token_trunc_bytes: int
+ token_scale_factor: int
+ scaled_trunc_tokens_per_trunc_sat: int
+ num_sats_trunc_bytes: int
+ maker_pk: bytes
+ min_accepted_scaled_trunc_tokens: int
+ token_id: str
+ token_type: int
+ token_protocol: str
+ script_len: int
+ dust_amount: int
+
+ @classmethod
+ def parse_redeem_script(cls, redeem_script, token):
+ consts = next(iter(redeem_script))
+ len_slp_intro = len(
+ slp_send(
+ token_type=token.token_type,
+ token_id=token.token_id,
+ amounts=[0],
+ )
+ )
+ ad_pushdata = consts[len_slp_intro:]
+ return parse_partial(ad_pushdata, token)
+
+ def ad_pushdata(self):
+ pushdata = bytearray()
+ pushdata.append(self.num_token_trunc_bytes)
+ pushdata.append(self.num_sats_trunc_bytes)
+ pushdata.extend(self.token_scale_factor.to_bytes(8, "little"))
+ pushdata.extend(self.scaled_trunc_tokens_per_trunc_sat.to_bytes(8, "little"))
+ pushdata.extend(self.min_accepted_scaled_trunc_tokens.to_bytes(8, "little"))
+ pushdata.extend(self.maker_pk)
+ return bytes(pushdata)
+
+ def data(self) -> list[bytes]:
+ return [
+ b"PARTIAL",
+ bytes([self.num_token_trunc_bytes]),
+ bytes([self.num_sats_trunc_bytes]),
+ self.token_scale_factor.to_bytes(8, "little"),
+ self.scaled_trunc_tokens_per_trunc_sat.to_bytes(8, "little"),
+ self.min_accepted_scaled_trunc_tokens.to_bytes(8, "little"),
+ ]
+
+ def script(self) -> CScript:
+ # See partial.ts in ecash-agora for a commented version of this Script
+ scaled_trunc_tokens_8le = (
+ self.trunc_tokens * self.token_scale_factor
+ ).to_bytes(8, "little")
+
+ ad_pushdata = self.ad_pushdata()
+
+ # Consts are of slightly different form
+ if self.token_protocol == "SLP":
+ slp_intro = slp_send(
+ token_type=self.token_type,
+ token_id=self.token_id,
+ amounts=[0],
+ )
+ covenant_consts = bytes(slp_intro) + ad_pushdata
+ token_intro_len = len(slp_intro)
+ else:
+ raise NotImplementedError
+
+ return CScript(
+ [
+ covenant_consts,
+ scaled_trunc_tokens_8le,
+ OP_CODESEPARATOR,
+ OP_ROT,
+ OP_IF,
+ OP_BIN2NUM,
+ OP_ROT,
+ OP_2DUP,
+ OP_GREATERTHANOREQUAL,
+ OP_VERIFY,
+ OP_DUP,
+ self.min_accepted_scaled_trunc_tokens,
+ OP_GREATERTHANOREQUAL,
+ OP_VERIFY,
+ OP_DUP,
+ self.token_scale_factor,
+ OP_MOD,
+ OP_0,
+ OP_EQUALVERIFY,
+ OP_TUCK,
+ OP_SUB,
+ 2,
+ OP_PICK,
+ token_intro_len,
+ OP_SPLIT,
+ OP_DROP,
+ OP_OVER,
+ OP_0NOTEQUAL,
+ *self._script_build_op_return(),
+ bytes(self.num_sats_trunc_bytes),
+ OP_CAT,
+ OP_ROT,
+ self.scaled_trunc_tokens_per_trunc_sat - 1,
+ OP_ADD,
+ self.scaled_trunc_tokens_per_trunc_sat,
+ OP_DIV,
+ 8 - self.num_sats_trunc_bytes,
+ OP_NUM2BIN,
+ OP_CAT,
+ bytes([25, OP_DUP, OP_HASH160, 20]),
+ OP_2OVER,
+ OP_DROP,
+ len(covenant_consts) - 33,
+ OP_SPLIT,
+ OP_NIP,
+ OP_HASH160,
+ OP_CAT,
+ bytes([OP_EQUALVERIFY, OP_CHECKSIG]),
+ OP_CAT,
+ OP_CAT,
+ OP_TOALTSTACK,
+ OP_TUCK,
+ self.dust_amount,
+ OP_8,
+ OP_NUM2BIN,
+ bytes([23, OP_HASH160, 20]),
+ OP_CAT,
+ bytes(
+ [OP_PUSHDATA1, len(covenant_consts)]
+ if len(covenant_consts) >= OP_PUSHDATA1
+ else [len(covenant_consts)]
+ ),
+ OP_2SWAP,
+ OP_8,
+ OP_TUCK,
+ OP_NUM2BIN,
+ OP_CAT,
+ OP_CAT,
+ OP_CAT,
+ bytes([OP_CODESEPARATOR]),
+ OP_CAT,
+ 3,
+ OP_PICK,
+ 36 + (1 if self.script_len < 0xFD else 3),
+ OP_SPLIT,
+ OP_NIP,
+ self.script_len,
+ OP_SPLIT,
+ OP_12,
+ OP_SPLIT,
+ OP_NIP,
+ 32,
+ OP_SPLIT,
+ OP_DROP,
+ OP_TOALTSTACK,
+ OP_CAT,
+ OP_HASH160,
+ OP_CAT,
+ bytes([OP_EQUAL]),
+ OP_CAT,
+ OP_SWAP,
+ OP_0NOTEQUAL,
+ OP_NOTIF,
+ OP_DROP,
+ b"",
+ OP_ENDIF,
+ OP_ROT,
+ OP_SIZE,
+ OP_0NOTEQUAL,
+ OP_VERIFY,
+ OP_CAT,
+ OP_FROMALTSTACK,
+ OP_FROMALTSTACK,
+ OP_ROT,
+ OP_CAT,
+ OP_HASH256,
+ OP_EQUALVERIFY,
+ OP_2,
+ 4 + 32 + 32,
+ OP_NUM2BIN,
+ OP_SWAP,
+ OP_CAT,
+ OP_SHA256,
+ OP_3DUP,
+ OP_ROT,
+ OP_CHECKDATASIGVERIFY,
+ OP_DROP,
+ bytes([ALL_ANYONECANPAY_BIP143]),
+ OP_CAT,
+ OP_SWAP,
+ OP_ELSE,
+ OP_DROP,
+ len(covenant_consts) - 33,
+ OP_SPLIT,
+ OP_NIP,
+ OP_ENDIF,
+ *self._script_outro(),
+ ]
+ )
+
+ def _script_build_op_return(self):
+ if self.token_protocol == "SLP":
+ return self._script_build_slp_op_return()
+ else:
+ raise NotImplementedError
+
+ def _script_build_slp_op_return(self):
+ return [
+ OP_IF,
+ OP_8,
+ OP_CAT,
+ OP_OVER,
+ self.token_scale_factor,
+ OP_DIV,
+ *self._script_ser_slp_trunc_tokens(),
+ OP_REVERSEBYTES,
+ bytes(self.num_token_trunc_bytes),
+ OP_CAT,
+ OP_CAT,
+ OP_ENDIF,
+ OP_8,
+ OP_CAT,
+ 2,
+ OP_PICK,
+ self.token_scale_factor,
+ OP_DIV,
+ *self._script_ser_slp_trunc_tokens(),
+ OP_REVERSEBYTES,
+ bytes(self.num_token_trunc_bytes),
+ OP_CAT,
+ OP_CAT,
+ OP_SIZE,
+ OP_9,
+ OP_NUM2BIN,
+ OP_REVERSEBYTES,
+ OP_SWAP,
+ OP_CAT,
+ ]
+
+ def _script_ser_slp_trunc_tokens(self):
+ if self.num_token_trunc_bytes == 5:
+ return [
+ 4,
+ OP_NUM2BIN,
+ 3,
+ OP_SPLIT,
+ OP_DROP,
+ ]
+ return [
+ 8 - self.num_token_trunc_bytes,
+ OP_NUM2BIN,
+ ]
+
+ def _script_outro(self):
+ if self.token_protocol == "SLP":
+ return [
+ OP_CHECKSIGVERIFY,
+ b"PARTIAL",
+ OP_EQUALVERIFY,
+ LOKAD_ID,
+ OP_EQUAL,
+ ]
+ else:
+ raise NotImplementedError
+
+
+def parse_partial(pushdata: bytes, token) -> Optional[AgoraPartial]:
+ data_reader = BytesIO(pushdata)
+ # AGR0 PARTIAL pushdata always has the same length
+ if token.token_protocol == "SLP":
+ if len(pushdata) != 59:
+ return None
+ else:
+ raise NotImplementedError
+ num_token_trunc_bytes = data_reader.read(1)[0]
+ num_sats_trunc_bytes = data_reader.read(1)[0]
+ token_scale_factor = int.from_bytes(data_reader.read(8), "little")
+ scaled_trunc_tokens_per_trunc_sat = int.from_bytes(data_reader.read(8), "little")
+ min_accepted_scaled_trunc_tokens = int.from_bytes(data_reader.read(8), "little")
+ maker_pk = data_reader.read(33)
+
+ token_trunc_factor = 1 << (8 * num_token_trunc_bytes)
+
+ # Offers must have a losslessly truncatable token amount
+ if token.amount % token_trunc_factor != 0:
+ return None
+
+ partial_alp = AgoraPartial(
+ trunc_tokens=token.amount // token_trunc_factor,
+ num_token_trunc_bytes=num_token_trunc_bytes,
+ token_scale_factor=token_scale_factor,
+ scaled_trunc_tokens_per_trunc_sat=scaled_trunc_tokens_per_trunc_sat,
+ num_sats_trunc_bytes=num_sats_trunc_bytes,
+ maker_pk=maker_pk,
+ min_accepted_scaled_trunc_tokens=min_accepted_scaled_trunc_tokens,
+ token_id=token.token_id,
+ token_type=token.token_type,
+ token_protocol=token.token_protocol,
+ script_len=0x7F,
+ dust_amount=546,
+ )
+ measured_len = len(cut_out_codesep(partial_alp.script()))
+ if measured_len > 0x80:
+ partial_alp.script_len = measured_len
+ measured_len = len(cut_out_codesep(partial_alp.script()))
+ partial_alp.script_len = measured_len
+ return partial_alp
+
+
+def cut_out_codesep(script):
+ script_iter = iter(script)
+ for op in script_iter:
+ if op == OP_CODESEPARATOR:
+ break
+ else:
+ return script
+ return CScript(script_iter)
diff --git a/modules/ecash-agora/src/agora.ts b/modules/ecash-agora/src/agora.ts
--- a/modules/ecash-agora/src/agora.ts
+++ b/modules/ecash-agora/src/agora.ts
@@ -2,8 +2,15 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-import { ChronikClient, PluginEndpoint, Token, Utxo } from 'chronik-client';
import {
+ ChronikClient,
+ PluginEndpoint,
+ PluginEntry,
+ Token,
+ Utxo,
+} from 'chronik-client';
+import {
+ Amount,
Bytes,
DEFAULT_DUST_LIMIT,
DEFAULT_FEE_PER_KB,
@@ -13,6 +20,8 @@
OutPoint,
readTxOutput,
Script,
+ shaRmd160,
+ Signatory,
slpSend,
strToBytes,
toHex,
@@ -29,6 +38,11 @@
AgoraOneshotCancelSignatory,
AgoraOneshotSignatory,
} from './oneshot.js';
+import {
+ AgoraPartial,
+ AgoraPartialCancelSignatory,
+ AgoraPartialSignatory,
+} from './partial.js';
const TOKEN_ID_PREFIX = toHex(strToBytes('T'));
const PUBKEY_PREFIX = toHex(strToBytes('P'));
@@ -37,14 +51,20 @@
const PLUGIN_NAME = 'agora';
const ONESHOT_HEX = toHex(strToBytes(AgoraOneshot.COVENANT_VARIANT));
+const PARTIAL_HEX = toHex(strToBytes(AgoraPartial.COVENANT_VARIANT));
const PLUGIN_GROUPS_MAX_PAGE_SIZE = 50;
/** Offer variant, determines the Script used to enforce the offer */
-export type AgoraOfferVariant = {
- type: 'ONESHOT';
- params: AgoraOneshot;
-};
+export type AgoraOfferVariant =
+ | {
+ type: 'ONESHOT';
+ params: AgoraOneshot;
+ }
+ | {
+ type: 'PARTIAL';
+ params: AgoraPartial;
+ };
/**
* Individual token offer on the Agora, i.e. one UTXO offering tokens.
@@ -101,6 +121,8 @@
fuelInputs: TxBuilderInput[];
/** Script to send the tokens and the leftover sats (if any) to. */
recipientScript: Script;
+ /** For partial offers: Number of accepted tokens */
+ acceptedTokens?: bigint;
/** Dust amount to use for the token output. */
dustAmount?: number;
/** Fee per kB to use when building the tx. */
@@ -119,6 +141,7 @@
},
params.recipientScript,
],
+ acceptedTokens: params.acceptedTokens,
});
return txBuild.sign(params.ecc, feePerKb, dustAmount);
}
@@ -136,6 +159,7 @@
extraInputs?: TxBuilderInput[];
/** Fee per kB to use when building the tx. */
feePerKb?: number;
+ acceptedTokens?: bigint;
}): bigint {
const feePerKb = params.feePerKb ?? DEFAULT_FEE_PER_KB;
const txBuild = this._acceptTxBuilder({
@@ -148,6 +172,7 @@
script: params.recipientScript,
},
],
+ acceptedTokens: params.acceptedTokens,
});
const measureTx = txBuild.sign(new EccDummy());
return BigInt(Math.ceil((measureTx.serSize() * feePerKb) / 1000));
@@ -158,24 +183,93 @@
covenantPk: Uint8Array;
fuelInputs: TxBuilderInput[];
extraOutputs: TxBuilderOutput[];
- }) {
- return new TxBuilder({
- inputs: [
- ...params.fuelInputs,
- {
+ acceptedTokens?: bigint;
+ }): TxBuilder {
+ switch (this.variant.type) {
+ case 'ONESHOT':
+ return new TxBuilder({
+ inputs: [
+ ...params.fuelInputs,
+ {
+ input: this.txBuilderInput,
+ signatory: AgoraOneshotSignatory(
+ params.covenantSk,
+ params.covenantPk,
+ this.variant.params.enforcedOutputs.length,
+ ),
+ },
+ ],
+ outputs: [
+ ...this.variant.params.enforcedOutputs,
+ ...params.extraOutputs,
+ ],
+ });
+ case 'PARTIAL':
+ if (params.acceptedTokens === undefined) {
+ throw new Error(
+ 'Must set acceptedTokens for partial offers',
+ );
+ }
+ const txBuild = new TxBuilder();
+ const agoraPartial = this.variant.params;
+ const truncFactor =
+ 1n << BigInt(8 * agoraPartial.numTokenTruncBytes);
+ if (params.acceptedTokens % truncFactor != 0n) {
+ throw new Error(
+ `Must acceptedTokens must be a multiple of ${truncFactor}`,
+ );
+ }
+ txBuild.inputs.push({
input: this.txBuilderInput,
- signatory: AgoraOneshotSignatory(
+ signatory: AgoraPartialSignatory(
+ agoraPartial,
+ params.acceptedTokens / truncFactor,
params.covenantSk,
params.covenantPk,
- this.variant.params.enforcedOutputs.length,
),
- },
- ],
- outputs: [
- ...this.variant.params.enforcedOutputs,
- ...params.extraOutputs,
- ],
- });
+ });
+ txBuild.inputs.push(...params.fuelInputs);
+ const sendAmounts: Amount[] = [0];
+ const offeredTokens = BigInt(this.token.amount);
+ if (offeredTokens > params.acceptedTokens) {
+ sendAmounts.push(offeredTokens - params.acceptedTokens);
+ }
+ sendAmounts.push(params.acceptedTokens);
+ if (agoraPartial.tokenProtocol == 'SLP') {
+ txBuild.outputs.push({
+ value: 0,
+ script: slpSend(
+ this.token.tokenId,
+ this.token.tokenType.number,
+ sendAmounts,
+ ),
+ });
+ } else {
+ throw new Error('Only SLP implemented at the moment');
+ }
+ txBuild.outputs.push({
+ value: agoraPartial.askedSats(params.acceptedTokens),
+ script: Script.p2pkh(shaRmd160(agoraPartial.makerPk)),
+ });
+ if (offeredTokens > params.acceptedTokens) {
+ const newAgoraPartial = new AgoraPartial({
+ ...agoraPartial,
+ truncTokens:
+ (offeredTokens - params.acceptedTokens) /
+ truncFactor,
+ });
+ txBuild.outputs.push({
+ value: agoraPartial.dustAmount,
+ script: Script.p2sh(
+ shaRmd160(newAgoraPartial.script().bytecode),
+ ),
+ });
+ }
+ txBuild.outputs.push(...params.extraOutputs);
+ return txBuild;
+ default:
+ throw new Error('Not implemented');
+ }
}
/**
@@ -263,12 +357,26 @@
fuelInputs: TxBuilderInput[];
extraOutputs: TxBuilderOutput[];
}) {
+ let signatory: Signatory;
+ switch (this.variant.type) {
+ case 'ONESHOT':
+ signatory = AgoraOneshotCancelSignatory(params.cancelSk);
+ break;
+ case 'PARTIAL':
+ signatory = AgoraPartialCancelSignatory(
+ params.cancelSk,
+ this.variant.params.tokenProtocol,
+ );
+ break;
+ default:
+ throw new Error('Not implemented');
+ }
return new TxBuilder({
inputs: [
...params.fuelInputs,
{
input: this.txBuilderInput,
- signatory: AgoraOneshotCancelSignatory(params.cancelSk),
+ signatory,
},
],
outputs: [
@@ -289,8 +397,20 @@
* How many satoshis are asked to accept this offer, excluding tx fees.
* This is what should be displayed to the user as the price.
**/
- public askedSats(): bigint {
- return this.variant.params.askedSats();
+ public askedSats(acceptedTokens?: bigint): bigint {
+ switch (this.variant.type) {
+ case 'ONESHOT':
+ return this.variant.params.askedSats();
+ case 'PARTIAL':
+ if (acceptedTokens === undefined) {
+ throw new Error(
+ 'Must provide acceptedTokens for PARTIAL offers',
+ );
+ }
+ return this.variant.params.askedSats(acceptedTokens);
+ default:
+ throw new Error('Not implemented');
+ }
}
}
@@ -302,13 +422,15 @@
**/
export class Agora {
private plugin: PluginEndpoint;
+ private dustAmount: number;
/**
* Create an Agora instance. The provided Chronik instance must have the
* "agora" plugin loaded.
**/
- public constructor(chronik: ChronikClient) {
+ public constructor(chronik: ChronikClient, dustAmount?: number) {
this.plugin = chronik.plugin(PLUGIN_NAME);
+ this.dustAmount = dustAmount ?? DEFAULT_DUST_LIMIT;
}
/**
@@ -386,17 +508,27 @@
if (utxo.plugins === undefined) {
return undefined;
}
- if (utxo.token?.tokenType.protocol !== 'SLP') {
- // Currently only SLP supported
- return undefined;
- }
const plugin = utxo.plugins[PLUGIN_NAME];
if (plugin === undefined) {
return undefined;
}
const covenantVariant = plugin.data[0];
- if (covenantVariant !== ONESHOT_HEX) {
- // Unknown offer type
+ switch (covenantVariant) {
+ case ONESHOT_HEX:
+ return this._parseOneshotOfferUtxo(utxo, plugin);
+ case PARTIAL_HEX:
+ return this._parsePartialOfferUtxo(utxo, plugin);
+ default:
+ return undefined;
+ }
+ }
+
+ private _parseOneshotOfferUtxo(
+ utxo: Utxo,
+ plugin: PluginEntry,
+ ): AgoraOffer | undefined {
+ if (utxo.token?.tokenType.protocol !== 'SLP') {
+ // Currently only SLP supported
return undefined;
}
const outputsSerHex = plugin.data[1];
@@ -443,4 +575,81 @@
token: utxo.token,
});
}
+
+ private _parsePartialOfferUtxo(
+ utxo: Utxo,
+ plugin: PluginEntry,
+ ): AgoraOffer | undefined {
+ if (utxo.token === undefined) {
+ return undefined;
+ }
+ if (utxo.token.tokenType.protocol !== 'SLP') {
+ // Currently only SLP supported
+ return undefined;
+ }
+
+ // Plugin gives us the offer data in this form
+ const [
+ _,
+ numTokenTruncBytesHex,
+ numSatsTruncBytesHex,
+ tokenScaleFactorHex,
+ scaledTruncTokensPerTruncSatHex,
+ minAcceptedScaledTruncTokensHex,
+ ] = plugin.data;
+
+ const numTokenTruncBytes = fromHex(numTokenTruncBytesHex)[0];
+ const numSatsTruncBytes = fromHex(numSatsTruncBytesHex)[0];
+ const tokenScaleFactor = new Bytes(
+ fromHex(tokenScaleFactorHex),
+ ).readU64();
+ const scaledTruncTokensPerTruncSat = new Bytes(
+ fromHex(scaledTruncTokensPerTruncSatHex),
+ ).readU64();
+ const minAcceptedScaledTruncTokens = new Bytes(
+ fromHex(minAcceptedScaledTruncTokensHex),
+ ).readU64();
+
+ const makerPkGroupHex = plugin.groups.find(group =>
+ group.startsWith(PUBKEY_PREFIX),
+ );
+ if (makerPkGroupHex === undefined) {
+ return undefined;
+ }
+ const makerPk = fromHex(
+ makerPkGroupHex.substring(PUBKEY_PREFIX.length),
+ );
+
+ const agoraPartial = new AgoraPartial({
+ truncTokens:
+ BigInt(utxo.token.amount) >> (8n * BigInt(numTokenTruncBytes)),
+ numTokenTruncBytes,
+ tokenScaleFactor,
+ scaledTruncTokensPerTruncSat,
+ numSatsTruncBytes,
+ makerPk,
+ minAcceptedScaledTruncTokens,
+ tokenId: utxo.token.tokenId,
+ tokenType: utxo.token.tokenType.number,
+ tokenProtocol: utxo.token.tokenType.protocol,
+ scriptLen: 0x7f,
+ dustAmount: this.dustAmount,
+ });
+ agoraPartial.updateScriptLen();
+ return new AgoraOffer({
+ variant: {
+ type: 'PARTIAL',
+ params: agoraPartial,
+ },
+ outpoint: utxo.outpoint,
+ txBuilderInput: {
+ prevOut: utxo.outpoint,
+ signData: {
+ value: utxo.value,
+ redeemScript: agoraPartial.script(),
+ },
+ },
+ token: utxo.token,
+ });
+ }
}
diff --git a/modules/ecash-agora/src/partial.ts b/modules/ecash-agora/src/partial.ts
--- a/modules/ecash-agora/src/partial.ts
+++ b/modules/ecash-agora/src/partial.ts
@@ -1043,4 +1043,118 @@
throw new Error('Only SLP implemented');
}
}
+
+ /**
+ * redeemScript of the Script advertizing this offer.
+ * It requires a setup tx followed by the actual offer, which reveals
+ * the covenantConsts.
+ * The reason we have an OP_CHECKSIGVERIFY (as opposed to just leaving it
+ * as "anyone can spend with this pushdata") is so that others on the
+ * network can't spend this UTXO (and potentially take the tokens in it),
+ * and only the maker can spend it.
+ **/
+ public adScript(): Script {
+ const [covenantConsts, _] = this.covenantConsts();
+ return Script.fromOps([
+ pushBytesOp(covenantConsts),
+ pushNumberOp(covenantConsts.length - 33),
+ OP_SPLIT,
+ OP_NIP,
+ OP_CHECKSIGVERIFY,
+ pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
+ OP_EQUALVERIFY,
+ pushBytesOp(AGORA_LOKAD_ID),
+ OP_EQUAL,
+ ]);
+ }
+}
+
+function makeScriptSigIntro(tokenProtocol: 'SLP' | 'ALP'): Op[] {
+ switch (tokenProtocol) {
+ case 'SLP':
+ // For SLP, we need to add "AGR0" "PARTIAL" at the beginning of the
+ // scriptSig, to advertize it via the LOKAD ID. ALP uses the
+ // OP_RETURN, so there this is not needed.
+ return [
+ pushBytesOp(AGORA_LOKAD_ID),
+ pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
+ ];
+ default:
+ return [];
+ }
}
+
+export const AgoraPartialSignatory = (
+ params: AgoraPartial,
+ acceptedTruncTokens: bigint,
+ covenantSk: Uint8Array,
+ covenantPk: Uint8Array,
+): Signatory => {
+ return (ecc: Ecc, input: UnsignedTxInput) => {
+ const preimage = input.sigHashPreimage(ALL_ANYONECANPAY_BIP143, 0);
+ const sighash = sha256d(preimage.bytes);
+ const covenantSig = ecc.schnorrSign(covenantSk, sighash);
+ const hasLeftover = params.truncTokens > acceptedTruncTokens;
+ const buyerOutputIdx = hasLeftover ? 3 : 2;
+ const buyerOutputs = input.unsignedTx.tx.outputs.slice(buyerOutputIdx);
+
+ const serTakerOutputs = (writer: Writer) => {
+ for (const output of buyerOutputs) {
+ writeTxOutput(output, writer);
+ }
+ };
+ const writerLength = new WriterLength();
+ serTakerOutputs(writerLength);
+ const writer = new WriterBytes(writerLength.length);
+ serTakerOutputs(writer);
+ const buyerOutputsSer = writer.data;
+
+ return Script.fromOps([
+ ...makeScriptSigIntro(params.tokenProtocol),
+ pushBytesOp(covenantPk),
+ pushBytesOp(covenantSig),
+ pushBytesOp(buyerOutputsSer),
+ pushBytesOp(preimage.bytes.slice(4 + 32 + 32)), // preimage_4_10
+ pushNumberOp(acceptedTruncTokens * params.tokenScaleFactor),
+ OP_1, // is_purchase = true
+ pushBytesOp(preimage.redeemScript.bytecode),
+ ]);
+ };
+};
+
+export const AgoraPartialCancelSignatory = (
+ makerSk: Uint8Array,
+ tokenProtocol: 'SLP' | 'ALP',
+): Signatory => {
+ return (ecc: Ecc, input: UnsignedTxInput) => {
+ const preimage = input.sigHashPreimage(ALL_BIP143, 0);
+ const sighash = sha256d(preimage.bytes);
+ const cancelSig = flagSignature(
+ ecc.schnorrSign(makerSk, sighash),
+ ALL_BIP143,
+ );
+ return Script.fromOps([
+ ...makeScriptSigIntro(tokenProtocol),
+ pushBytesOp(cancelSig),
+ OP_0, // is_purchase = false
+ pushBytesOp(preimage.redeemScript.bytecode),
+ ]);
+ };
+};
+
+export const AgoraPartialAdSignatory = (makerSk: Uint8Array) => {
+ return (ecc: Ecc, input: UnsignedTxInput): Script => {
+ const preimage = input.sigHashPreimage(ALL_BIP143);
+ const sighash = sha256d(preimage.bytes);
+ const makerSig = flagSignature(
+ ecc.schnorrSign(makerSk, sighash),
+ ALL_BIP143,
+ );
+ return Script.fromOps([
+ pushBytesOp(AGORA_LOKAD_ID),
+ pushBytesOp(strToBytes(AgoraPartial.COVENANT_VARIANT)),
+ pushBytesOp(makerSig),
+ pushBytesOp(preimage.redeemScript.bytecode),
+ ]);
+ };
+};
diff --git a/modules/ecash-agora/tests/partial-helper-slp.ts b/modules/ecash-agora/tests/partial-helper-slp.ts
new file mode 100644
--- /dev/null
+++ b/modules/ecash-agora/tests/partial-helper-slp.ts
@@ -0,0 +1,154 @@
+// 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 { ChronikClient } from 'chronik-client';
+import {
+ ALL_BIP143,
+ Ecc,
+ P2PKHSignatory,
+ Script,
+ shaRmd160,
+ slpGenesis,
+ slpSend,
+ TxBuilder,
+ TxBuilderInput,
+} from 'ecash-lib';
+import { expect } from 'chai';
+
+import { AgoraPartial, AgoraPartialAdSignatory } from '../src/partial.js';
+import { Agora, AgoraOffer } from '../src/agora.js';
+
+export async function makeSlpOffer(params: {
+ chronik: ChronikClient;
+ ecc: Ecc;
+ agoraPartial: AgoraPartial;
+ makerSk: Uint8Array;
+ fuelInput: TxBuilderInput;
+}): Promise<AgoraOffer> {
+ const { chronik, ecc, agoraPartial, makerSk, fuelInput } = params;
+ const makerPk = ecc.derivePubkey(makerSk);
+ const makerPkh = shaRmd160(makerPk);
+ const makerP2pkh = Script.p2pkh(makerPkh);
+
+ const genesisOutputSats = 2000;
+ const txBuildGenesisGroup = new TxBuilder({
+ inputs: [fuelInput],
+ outputs: [
+ {
+ value: 0,
+ script: slpGenesis(
+ agoraPartial.tokenType,
+ {
+ tokenTicker: `SLP token type ${agoraPartial.tokenType}`,
+ decimals: 4,
+ },
+ agoraPartial.offeredTokens(),
+ ),
+ },
+ { value: genesisOutputSats, script: makerP2pkh },
+ ],
+ });
+ const genesisTx = txBuildGenesisGroup.sign(ecc);
+ const genesisTxid = (await chronik.broadcastTx(genesisTx.ser())).txid;
+ const tokenId = genesisTxid;
+ agoraPartial.tokenId = tokenId;
+
+ expect((await chronik.token(tokenId)).tokenType.number).to.equal(
+ agoraPartial.tokenType,
+ );
+
+ const adSetupSats = 1000n;
+ const agoraAdScript = agoraPartial.adScript();
+ const agoraAdP2sh = Script.p2sh(shaRmd160(agoraAdScript.bytecode));
+ const txBuildAdSetup = new TxBuilder({
+ inputs: [
+ {
+ input: {
+ prevOut: {
+ txid: genesisTxid,
+ outIdx: 1,
+ },
+ signData: {
+ value: genesisOutputSats,
+ outputScript: makerP2pkh,
+ },
+ },
+ signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143),
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ script: slpSend(tokenId, agoraPartial.tokenType, [
+ agoraPartial.offeredTokens(),
+ ]),
+ },
+ { value: adSetupSats, script: agoraAdP2sh },
+ ],
+ });
+ const adSetupTx = txBuildAdSetup.sign(ecc);
+ const adSetupTxid = (await chronik.broadcastTx(adSetupTx.ser())).txid;
+
+ const agoraScript = agoraPartial.script();
+ const agoraP2sh = Script.p2sh(shaRmd160(agoraScript.bytecode));
+ const txBuildOffer = new TxBuilder({
+ inputs: [
+ {
+ input: {
+ prevOut: {
+ txid: adSetupTxid,
+ outIdx: 1,
+ },
+ signData: {
+ value: adSetupSats,
+ redeemScript: agoraAdScript,
+ },
+ },
+ signatory: AgoraPartialAdSignatory(makerSk),
+ },
+ ],
+ outputs: [
+ {
+ value: 0,
+ script: slpSend(tokenId, agoraPartial.tokenType, [
+ agoraPartial.offeredTokens(),
+ ]),
+ },
+ { value: 546, script: agoraP2sh },
+ ],
+ });
+ const offerTx = txBuildOffer.sign(ecc);
+ await chronik.broadcastTx(offerTx.ser());
+
+ const agora = new Agora(chronik);
+ expect(await agora.offeredFungibleTokenIds()).to.include(tokenId);
+ const offers = await agora.activeOffersByTokenId(tokenId);
+ expect(offers.length).to.equal(1);
+
+ return offers[0];
+}
+
+export async function takeSlpOffer(params: {
+ chronik: ChronikClient;
+ ecc: Ecc;
+ offer: AgoraOffer;
+ takerSk: Uint8Array;
+ takerInput: TxBuilderInput;
+ acceptedTokens: bigint;
+}) {
+ const takerSk = params.takerSk;
+ const takerPk = params.ecc.derivePubkey(takerSk);
+ const takerPkh = shaRmd160(takerPk);
+ const takerP2pkh = Script.p2pkh(takerPkh);
+ const acceptTx = params.offer.acceptTx({
+ ecc: params.ecc,
+ covenantSk: params.takerSk,
+ covenantPk: takerPk,
+ fuelInputs: [params.takerInput],
+ recipientScript: takerP2pkh,
+ acceptedTokens: params.acceptedTokens,
+ });
+ const acceptTxid = (await params.chronik.broadcastTx(acceptTx.ser())).txid;
+ return acceptTxid;
+}
diff --git a/modules/ecash-agora/tests/partial.slp.bigsats.test.ts b/modules/ecash-agora/tests/partial.slp.bigsats.test.ts
new file mode 100644
--- /dev/null
+++ b/modules/ecash-agora/tests/partial.slp.bigsats.test.ts
@@ -0,0 +1,681 @@
+// 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_LIMIT,
+ Ecc,
+ P2PKHSignatory,
+ SLP_FUNGIBLE,
+ SLP_LOKAD_ID,
+ Script,
+ TxBuilderInput,
+ fromHex,
+ initWasm,
+ shaRmd160,
+ slpSend,
+ strToBytes,
+ 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';
+
+use(chaiAsPromised);
+
+const BASE_PARAMS_SLP = {
+ tokenId: '00'.repeat(32), // filled in later
+ tokenType: SLP_FUNGIBLE,
+ tokenProtocol: 'SLP' as const,
+ dustAmount: DEFAULT_DUST_LIMIT,
+};
+
+const BIGSATS = 149 * 5000000000 - 20000;
+
+let makerSk: Uint8Array;
+let makerPk: Uint8Array;
+let makerPkh: Uint8Array;
+let makerScript: Script;
+let makerScriptHex: string;
+let takerSk: Uint8Array;
+let takerPk: Uint8Array;
+let takerPkh: Uint8Array;
+let takerScript: Script;
+let takerScriptHex: string;
+
+function initKeys(ecc: Ecc) {
+ makerSk = fromHex('33'.repeat(32));
+ makerPk = ecc.derivePubkey(makerSk);
+ makerPkh = shaRmd160(makerPk);
+ makerScript = Script.p2pkh(makerPkh);
+ makerScriptHex = toHex(makerScript.bytecode);
+ takerSk = fromHex('44'.repeat(32));
+ takerPk = ecc.derivePubkey(takerSk);
+ takerPkh = shaRmd160(takerPk);
+ takerScript = Script.p2pkh(takerPkh);
+ takerScriptHex = toHex(takerScript.bytecode);
+}
+
+async function makeBuilderInputs(
+ runner: TestRunner,
+ values: number[],
+): Promise<TxBuilderInput[]> {
+ const txid = await runner.sendToScript(values, makerScript);
+ return values.map((value, outIdx) => ({
+ input: {
+ prevOut: {
+ txid,
+ outIdx,
+ },
+ signData: {
+ value,
+ outputScript: makerScript,
+ },
+ },
+ signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143),
+ }));
+}
+
+describe('Agora Partial 7450M XEC vs 2p64-1 full accept', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ initKeys(ecc);
+ await runner.setupCoins(1, BIGSATS + 11000);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('Agora Partial 7450M XEC vs 2p64-1 full accept', async () => {
+ const [fuelInput, takerInput] = await makeBuilderInputs(runner, [
+ 10000,
+ BIGSATS,
+ ]);
+
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 0xffffffffffffffffn,
+ priceNanoSatsPerToken: 40n, // scaled to use the XEC
+ makerPk: makerPk,
+ minAcceptedTokens: 0xffffffffffffn,
+ ...BASE_PARAMS_SLP,
+ });
+
+ expect(agoraPartial).to.deep.equal(
+ new AgoraPartial({
+ truncTokens: 0xffffffn,
+ numTokenTruncBytes: 5,
+ tokenScaleFactor: 127n,
+ scaledTruncTokensPerTruncSat: 189n,
+ numSatsTruncBytes: 2,
+ makerPk,
+ minAcceptedScaledTruncTokens: 32511n,
+ ...BASE_PARAMS_SLP,
+ scriptLen: 216,
+ }),
+ );
+ expect(agoraPartial.offeredTokens()).to.equal(0xffffff0000000000n);
+ expect(agoraPartial.askedSats(0x10000000000n)).to.equal(65536n);
+ expect(agoraPartial.priceNanoSatsPerToken(0x10000000000n)).to.equal(
+ 59n,
+ );
+ expect(agoraPartial.askedSats(0xffffff0000000000n)).to.equal(
+ 738825273344n,
+ );
+ expect(
+ agoraPartial.priceNanoSatsPerToken(0xffffff0000000000n),
+ ).to.equal(40n);
+ expect(agoraPartial.priceNanoSatsPerToken()).to.equal(40n);
+
+ const offer = await makeSlpOffer({
+ chronik,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ offer,
+ takerSk,
+ takerInput,
+ acceptedTokens: agoraPartial.offeredTokens(),
+ });
+
+ 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, [
+ 0,
+ agoraPartial.offeredTokens(),
+ ]).bytecode,
+ ),
+ );
+ expect(acceptTx.outputs[0].value).to.equal(0);
+ 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].value).to.equal(738825273344);
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ // 2nd output is tokens to taker
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
+ 0xffffff0000000000n.toString(),
+ );
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript).to.equal(takerScriptHex);
+ });
+});
+
+describe('Agora Partial 7450M XEC vs 2p64-1 small accept', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ initKeys(ecc);
+ await runner.setupCoins(1, BIGSATS + 11000);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('Agora Partial 7450M XEC vs 2p64-1 small accept', async () => {
+ const [fuelInput, takerInput] = await makeBuilderInputs(runner, [
+ 10000,
+ BIGSATS,
+ ]);
+
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 0xffffffffffffffffn,
+ priceNanoSatsPerToken: 500000000n, // scaled to use the XEC
+ makerPk,
+ minAcceptedTokens: 0xffffffffffn,
+ ...BASE_PARAMS_SLP,
+ });
+ expect(agoraPartial).to.deep.equal(
+ new AgoraPartial({
+ truncTokens: 0xffffffn,
+ numTokenTruncBytes: 5,
+ tokenScaleFactor: 128n,
+ scaledTruncTokensPerTruncSat: 1n,
+ numSatsTruncBytes: 4,
+ makerPk,
+ minAcceptedScaledTruncTokens: 0x7fn,
+ ...BASE_PARAMS_SLP,
+ scriptLen: 216,
+ }),
+ );
+ expect(agoraPartial.offeredTokens()).to.equal(0xffffff0000000000n);
+ expect(agoraPartial.askedSats(0x10000000000n)).to.equal(549755813888n);
+ expect(agoraPartial.priceNanoSatsPerToken(0x10000000000n)).to.equal(
+ 500000000n,
+ );
+ expect(agoraPartial.askedSats(0xffffff0000000000n)).to.equal(
+ 9223371487098961920n,
+ );
+ expect(
+ agoraPartial.priceNanoSatsPerToken(0xffffff0000000000n),
+ ).to.equal(500000000n);
+ expect(agoraPartial.priceNanoSatsPerToken()).to.equal(500000000n);
+
+ const offer = await makeSlpOffer({
+ chronik,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptedTokens = 0x10000000000n;
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ offer,
+ takerSk,
+ takerInput,
+ acceptedTokens,
+ });
+
+ 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, [
+ 0,
+ agoraPartial.offeredTokens() - acceptedTokens,
+ acceptedTokens,
+ ]).bytecode,
+ ),
+ );
+ expect(acceptTx.outputs[0].value).to.equal(0);
+ 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].value).to.equal(549755813888);
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ // 2nd output is back to the P2SH Script
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
+ (agoraPartial.offeredTokens() - acceptedTokens).toString(),
+ );
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal('a914');
+ // 3rd output is tokens to taker
+ expect(acceptTx.outputs[3].token?.amount).to.equal(
+ acceptedTokens.toString(),
+ );
+ expect(acceptTx.outputs[3].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex);
+ });
+});
+
+describe('Agora Partial 7450M XEC vs 2p63-1 full accept', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ initKeys(ecc);
+ await runner.setupCoins(1, BIGSATS + 11000);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('Agora Partial 7450M XEC vs 2p63-1 full accept', async () => {
+ const [fuelInput, takerInput] = await makeBuilderInputs(runner, [
+ 10000,
+ BIGSATS,
+ ]);
+
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 0x7fffffffffffffffn,
+ priceNanoSatsPerToken: 80n, // scaled to use the XEC
+ makerPk: makerPk,
+ minAcceptedTokens: 0xffffffffffffn,
+ ...BASE_PARAMS_SLP,
+ });
+
+ expect(agoraPartial).to.deep.equal(
+ new AgoraPartial({
+ truncTokens: 0x7fffff42n,
+ numTokenTruncBytes: 4,
+ tokenScaleFactor: 1n,
+ scaledTruncTokensPerTruncSat: 190n,
+ numSatsTruncBytes: 2,
+ makerPk,
+ minAcceptedScaledTruncTokens: 0xffffn,
+ ...BASE_PARAMS_SLP,
+ scriptLen: 206,
+ }),
+ );
+ expect(agoraPartial.offeredTokens()).to.equal(0x7fffff4200000000n);
+ expect(agoraPartial.askedSats(0x100000000n)).to.equal(65536n);
+ expect(agoraPartial.priceNanoSatsPerToken(0x100000000n)).to.equal(
+ 15258n,
+ );
+ expect(agoraPartial.askedSats(0x7fffff4200000000n)).to.equal(
+ 740723589120n,
+ );
+ expect(
+ agoraPartial.priceNanoSatsPerToken(0x7fffff4200000000n),
+ ).to.equal(80n);
+ expect(agoraPartial.priceNanoSatsPerToken()).to.equal(80n);
+
+ const offer = await makeSlpOffer({
+ chronik,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptedTokens = agoraPartial.offeredTokens();
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ offer,
+ takerSk,
+ takerInput,
+ acceptedTokens,
+ });
+
+ 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, [
+ 0,
+ acceptedTokens,
+ ]).bytecode,
+ ),
+ );
+ expect(acceptTx.outputs[0].value).to.equal(0);
+ 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].value).to.equal(740723589120);
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ // 2nd output is tokens to taker
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
+ acceptedTokens.toString(),
+ );
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript).to.equal(takerScriptHex);
+ });
+});
+
+describe('Agora Partial 7450M XEC vs 2p63-1 small accept', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ initKeys(ecc);
+ await runner.setupCoins(1, BIGSATS + 11000);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('Agora Partial 7450M XEC vs 2p63-1 small accept', async () => {
+ const [fuelInput, takerInput] = await makeBuilderInputs(runner, [
+ 10000,
+ BIGSATS,
+ ]);
+
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 0x7fffffffffffffffn,
+ priceNanoSatsPerToken: 1000000000n,
+ makerPk,
+ minAcceptedTokens: 0x100000000n,
+ ...BASE_PARAMS_SLP,
+ });
+ expect(agoraPartial).to.deep.equal(
+ new AgoraPartial({
+ truncTokens: 0x7fffffffn,
+ numTokenTruncBytes: 4,
+ tokenScaleFactor: 1n,
+ scaledTruncTokensPerTruncSat: 1n,
+ numSatsTruncBytes: 4,
+ makerPk,
+ minAcceptedScaledTruncTokens: 1n,
+ ...BASE_PARAMS_SLP,
+ scriptLen: 201,
+ }),
+ );
+ expect(agoraPartial.offeredTokens()).to.equal(0x7fffffff00000000n);
+ expect(agoraPartial.askedSats(0x100000000n)).to.equal(4294967296n);
+ expect(agoraPartial.priceNanoSatsPerToken(0x100000000n)).to.equal(
+ 1000000000n,
+ );
+ expect(agoraPartial.askedSats(0x7fffffff00000000n)).to.equal(
+ 9223372032559808512n,
+ );
+ expect(
+ agoraPartial.priceNanoSatsPerToken(0x7fffffff00000000n),
+ ).to.equal(1000000000n);
+ expect(agoraPartial.priceNanoSatsPerToken()).to.equal(1000000000n);
+
+ const offer = await makeSlpOffer({
+ chronik,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptedTokens = 0x100000000n;
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ offer,
+ takerSk,
+ takerInput,
+ acceptedTokens,
+ });
+
+ 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, [
+ 0,
+ agoraPartial.offeredTokens() - acceptedTokens,
+ acceptedTokens,
+ ]).bytecode,
+ ),
+ );
+ expect(acceptTx.outputs[0].value).to.equal(0);
+ 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].value).to.equal(4294967296);
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ // 2nd output is back to the P2SH Script
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
+ (agoraPartial.offeredTokens() - acceptedTokens).toString(),
+ );
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal('a914');
+ // 3rd output is tokens to taker
+ expect(acceptTx.outputs[3].token?.amount).to.equal(
+ acceptedTokens.toString(),
+ );
+ expect(acceptTx.outputs[3].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex);
+ });
+});
+
+describe('Agora Partial 7450M XEC vs 100 full accept', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ initKeys(ecc);
+ await runner.setupCoins(1, BIGSATS + 11000);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('Agora Partial 7450M XEC vs 100 full accept', async () => {
+ const [fuelInput, takerInput] = await makeBuilderInputs(runner, [
+ 10000,
+ BIGSATS,
+ ]);
+
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 100n,
+ priceNanoSatsPerToken: 7123456780n * 1000000000n, // scaled to use the XEC
+ makerPk: makerPk,
+ minAcceptedTokens: 1n,
+ ...BASE_PARAMS_SLP,
+ });
+ expect(agoraPartial).to.deep.equal(
+ new AgoraPartial({
+ truncTokens: 100n,
+ numTokenTruncBytes: 0,
+ tokenScaleFactor: 0x7fff3a28n / 100n,
+ scaledTruncTokensPerTruncSat: 50576n,
+ numSatsTruncBytes: 3,
+ makerPk,
+ minAcceptedScaledTruncTokens: 0x7fff3a28n / 100n,
+ ...BASE_PARAMS_SLP,
+ scriptLen: 214,
+ }),
+ );
+ expect(agoraPartial.offeredTokens()).to.equal(100n);
+ expect(agoraPartial.minAcceptedTokens()).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,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ offer,
+ takerSk,
+ takerInput,
+ acceptedTokens: 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, [0, 100n])
+ .bytecode,
+ ),
+ );
+ expect(acceptTx.outputs[0].value).to.equal(0);
+ 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].value).to.equal(712360591360);
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ // 2nd output is tokens to taker
+ expect(acceptTx.outputs[2].token?.amount).to.equal('100');
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript).to.equal(takerScriptHex);
+ });
+});
+
+describe('Agora Partial 7450M XEC vs 100 small accept', () => {
+ let runner: TestRunner;
+ let chronik: ChronikClient;
+ let ecc: Ecc;
+
+ before(async () => {
+ await initWasm();
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
+ chronik = runner.chronik;
+ ecc = runner.ecc;
+ initKeys(ecc);
+ await runner.setupCoins(1, BIGSATS + 11000);
+ });
+
+ after(() => {
+ runner.stop();
+ });
+
+ it('Agora Partial 7450M XEC vs 100 small accept', async () => {
+ const [fuelInput, takerInput] = await makeBuilderInputs(runner, [
+ 10000,
+ BIGSATS,
+ ]);
+
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: 100n,
+ priceNanoSatsPerToken: 712345678000n * 1000000000n, // scaled to use the XEC
+ makerPk: makerPk,
+ minAcceptedTokens: 1n,
+ ...BASE_PARAMS_SLP,
+ });
+ expect(agoraPartial).to.deep.equal(
+ new AgoraPartial({
+ truncTokens: 100n,
+ numTokenTruncBytes: 0,
+ tokenScaleFactor: 0x7ffe05f4n / 100n,
+ scaledTruncTokensPerTruncSat: 129471n,
+ numSatsTruncBytes: 4,
+ makerPk,
+ minAcceptedScaledTruncTokens: 0x7ffe05f4n / 100n,
+ ...BASE_PARAMS_SLP,
+ scriptLen: 215,
+ }),
+ );
+ expect(agoraPartial.offeredTokens()).to.equal(100n);
+ expect(agoraPartial.minAcceptedTokens()).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,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ offer,
+ takerSk,
+ takerInput,
+ acceptedTokens: 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, [
+ 0,
+ 99n,
+ 1n,
+ ]).bytecode,
+ ),
+ );
+ expect(acceptTx.outputs[0].value).to.equal(0);
+ 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].value).to.equal(712964571136);
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ // 2nd output is back to the P2SH Script
+ expect(acceptTx.outputs[2].token?.amount).to.equal('99');
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal('a914');
+ // 3rd output is tokens to taker
+ expect(acceptTx.outputs[3].token?.amount).to.equal('1');
+ expect(acceptTx.outputs[3].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex);
+ });
+});
diff --git a/modules/ecash-agora/tests/partial.slp.test.ts b/modules/ecash-agora/tests/partial.slp.test.ts
--- a/modules/ecash-agora/tests/partial.slp.test.ts
+++ b/modules/ecash-agora/tests/partial.slp.test.ts
@@ -5,41 +5,626 @@
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { ChronikClient } from 'chronik-client';
-import { Ecc, initWasm, OP_RETURN, Script } from 'ecash-lib';
+import {
+ ALL_BIP143,
+ DEFAULT_DUST_LIMIT,
+ Ecc,
+ P2PKHSignatory,
+ SLP_FUNGIBLE,
+ Script,
+ TxBuilderInput,
+ fromHex,
+ initWasm,
+ 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);
// This test needs a lot of sats
const NUM_COINS = 500;
const COIN_VALUE = 1100000000;
+const BASE_PARAMS_SLP = {
+ tokenId: '00'.repeat(32), // filled in later
+ tokenType: SLP_FUNGIBLE,
+ tokenProtocol: 'SLP' as const,
+ dustAmount: DEFAULT_DUST_LIMIT,
+};
+
describe('AgoraPartial SLP', () => {
let runner: TestRunner;
let chronik: ChronikClient;
let ecc: Ecc;
+ let makerSk: Uint8Array;
+ let makerPk: Uint8Array;
+ let makerPkh: Uint8Array;
+ let makerScript: Script;
+ let makerScriptHex: string;
+ let takerSk: Uint8Array;
+ let takerPk: Uint8Array;
+ let takerPkh: Uint8Array;
+ let takerScript: Script;
+ let takerScriptHex: string;
+
+ async function makeBuilderInputs(
+ values: number[],
+ ): Promise<TxBuilderInput[]> {
+ const txid = await runner.sendToScript(values, makerScript);
+ return values.map((value, outIdx) => ({
+ input: {
+ prevOut: {
+ txid,
+ outIdx,
+ },
+ signData: {
+ value,
+ outputScript: makerScript,
+ },
+ },
+ signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143),
+ }));
+ }
+
before(async () => {
await initWasm();
runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
chronik = runner.chronik;
ecc = runner.ecc;
await runner.setupCoins(NUM_COINS, COIN_VALUE);
+
+ makerSk = fromHex('33'.repeat(32));
+ makerPk = ecc.derivePubkey(makerSk);
+ makerPkh = shaRmd160(makerPk);
+ makerScript = Script.p2pkh(makerPkh);
+ makerScriptHex = toHex(makerScript.bytecode);
+ takerSk = fromHex('44'.repeat(32));
+ takerPk = ecc.derivePubkey(takerSk);
+ takerPkh = shaRmd160(takerPk);
+ takerScript = Script.p2pkh(takerPkh);
+ takerScriptHex = toHex(takerScript.bytecode);
});
after(() => {
runner.stop();
});
- it('Can get a big UTXO', async () => {
- // TODO: this will be filled in by actual tests later
- const txid = await runner.sendToScript(
- [10000, 1010000000],
- Script.fromAddress(
- 'ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07',
- ),
- );
- const tx = await chronik.tx(txid);
- expect(tx.outputs[1].value).to.equal(1010000000);
- });
+ interface TestCase {
+ offeredTokens: bigint;
+ info: string;
+ priceNanoSatsPerToken: bigint;
+ acceptedTokens: bigint;
+ askedSats: number;
+ }
+ const TEST_CASES: TestCase[] = [
+ {
+ offeredTokens: 1000n,
+ info: '1sat/token, full accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 1000n,
+ askedSats: 1000,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1sat/token, dust accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 546n,
+ askedSats: 546,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000sat/token, full accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 1000n,
+ askedSats: 1000225,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000sat/token, half accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 500n,
+ askedSats: 500113,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000sat/token, 1 accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 1n,
+ askedSats: 1001,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000000sat/token, full accept',
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
+ acceptedTokens: 1000n,
+ askedSats: 1000013824,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000000sat/token, half accept',
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
+ acceptedTokens: 500n,
+ askedSats: 500039680,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000000sat/token, 1 accept',
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
+ acceptedTokens: 1n,
+ askedSats: 1048576,
+ },
+ {
+ offeredTokens: 1000n,
+ info: '1000000000sat/token, 1 accept',
+ priceNanoSatsPerToken: 1000000000n * 1000000000n,
+ acceptedTokens: 1n,
+ askedSats: 1006632960,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '0.001sat/token, full accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 1000000n,
+ askedSats: 1000,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1sat/token, full accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 1000000n,
+ askedSats: 1000000,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1sat/token, half accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 500000n,
+ askedSats: 500000,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1sat/token, dust accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 546n,
+ askedSats: 546,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1000sat/token, full accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 1000000n,
+ askedSats: 1001151232,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1000sat/token, half accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 500000n,
+ askedSats: 500575744,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1000sat/token, 1 accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 1n,
+ askedSats: 1024,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1000000sat/token, 1000 accept',
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
+ acceptedTokens: 1000n,
+ askedSats: 1005060096,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1000000sat/token, 1 accept',
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
+ acceptedTokens: 1n,
+ askedSats: 1048576,
+ },
+ {
+ offeredTokens: 1000000n,
+ info: '1000000sat/token, 1 accept',
+ priceNanoSatsPerToken: 1000000000n * 1000000000n,
+ acceptedTokens: 1n,
+ askedSats: 1006632960,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '0.001sat/token, full accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 1000000000n,
+ askedSats: 1000000,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '0.001sat/token, half accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 500000000n,
+ askedSats: 500000,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '0.001sat/token, dust accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 546000n,
+ askedSats: 546,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '1sat/token, full accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 1000000000n,
+ askedSats: 1000000000,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '1sat/token, half accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 500000000n,
+ askedSats: 500000000,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '1sat/token, dust accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 546n,
+ askedSats: 546,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '1000sat/token, 983040 accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 983040n,
+ askedSats: 989855744,
+ },
+ {
+ offeredTokens: 1000000000n,
+ info: '1000sat/token, 65536 accept',
+ priceNanoSatsPerToken: 1000n * 1000000000n,
+ acceptedTokens: 65536n,
+ askedSats: 67108864,
+ },
+ {
+ offeredTokens: 1000000000000n,
+ info: '0.000001sat/token, full accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 999999995904n,
+ askedSats: 1000108,
+ },
+ {
+ offeredTokens: 1000000000000n,
+ info: '0.000001sat/token, half accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 546045952n,
+ askedSats: 547,
+ },
+ {
+ offeredTokens: 1000000000000n,
+ info: '0.001sat/token, full accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 999999995904n,
+ askedSats: 1068115230,
+ },
+ {
+ offeredTokens: 1000000000000n,
+ info: '0.001sat/token, dust accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 589824n,
+ askedSats: 630,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '0.000000001sat/token, full accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 999999986991104n,
+ askedSats: 1000358,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '0.000000001sat/token, dust accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 546014494720n,
+ askedSats: 547,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '0.000001sat/token, full accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 999999986991104n,
+ askedSats: 1072883592,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '0.000001sat/token, dust accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 570425344n,
+ askedSats: 612,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '0.001sat/token, 1/1000 accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 999989182464n,
+ askedSats: 1004470272,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '0.001sat/token, min accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 0x1000000n,
+ askedSats: 65536,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '1sat/token, 1/1000000 accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 989855744n,
+ askedSats: 989855744,
+ },
+ {
+ offeredTokens: 1000000000000000n,
+ info: '1sat/token, min accept',
+ priceNanoSatsPerToken: 1000000000n,
+ acceptedTokens: 0x1000000n,
+ askedSats: 16777216,
+ },
+ {
+ offeredTokens: 1000000000000000000n,
+ info: '0.000000001sat/token, full accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 999999997191651328n,
+ askedSats: 1047737894,
+ },
+ {
+ offeredTokens: 1000000000000000000n,
+ info: '0.000000001sat/token, dust accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 558345748480n,
+ askedSats: 585,
+ },
+ {
+ offeredTokens: 1000000000000000000n,
+ info: '0.000001sat/token, 1/1000 accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 999997235527680n,
+ askedSats: 1002438656,
+ },
+ {
+ offeredTokens: 1000000000000000000n,
+ info: '0.000001sat/token, min accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 0x100000000n,
+ askedSats: 65536,
+ },
+ {
+ offeredTokens: 1000000000000000000n,
+ info: '0.001sat/token, 1/1000000 accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 996432412672n,
+ askedSats: 1006632960,
+ },
+ {
+ offeredTokens: 1000000000000000000n,
+ info: '0.001sat/token, min accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 0x100000000n,
+ askedSats: 16777216,
+ },
+ {
+ offeredTokens: 0x7fffffffffffffffn,
+ info: '0.000000001sat/token, max sats accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 999999997191651328n,
+ askedSats: 1010248448,
+ },
+ {
+ offeredTokens: 0x7fffffffffffffffn,
+ info: '0.000000001sat/token, dust accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 558345748480n,
+ askedSats: 768,
+ },
+ {
+ offeredTokens: 0x7fffffffffffffffn,
+ info: '0.000001sat/token, max sats accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 999997235527680n,
+ askedSats: 1017249792,
+ },
+ {
+ offeredTokens: 0x7fffffffffffffffn,
+ info: '0.000001sat/token, min accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 0x100000000n,
+ askedSats: 65536,
+ },
+ {
+ offeredTokens: 0x7fffffffffffffffn,
+ info: '0.001sat/token, max sats accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 0xc300000000n,
+ askedSats: 1090519040,
+ },
+ {
+ offeredTokens: 0x7fffffffffffffffn,
+ info: '0.001sat/token, min accept',
+ priceNanoSatsPerToken: 1000000n,
+ acceptedTokens: 0x100000000n,
+ askedSats: 16777216,
+ },
+ {
+ offeredTokens: 0xffffffffffffffffn,
+ info: '0.000000001sat/token, max sats accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 999999228392505344n,
+ askedSats: 1027665664,
+ },
+ {
+ offeredTokens: 0xffffffffffffffffn,
+ info: '0.000000001sat/token, dust accept',
+ priceNanoSatsPerToken: 1n,
+ acceptedTokens: 0x10000000000n,
+ askedSats: 1280,
+ },
+ {
+ offeredTokens: 0xffffffffffffffffn,
+ info: '0.000001sat/token, max sats accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 999456069648384n,
+ askedSats: 1089339392,
+ },
+ {
+ offeredTokens: 0xffffffffffffffffn,
+ info: '0.000001sat/token, min accept',
+ priceNanoSatsPerToken: 1000n,
+ acceptedTokens: 0x10000000000n,
+ askedSats: 1245184,
+ },
+ ];
+
+ for (const testCase of TEST_CASES) {
+ it(`AgoraPartial SLP ${testCase.offeredTokens} for ${testCase.info}`, async () => {
+ const agoraPartial = AgoraPartial.approximateParams({
+ offeredTokens: testCase.offeredTokens,
+ priceNanoSatsPerToken: testCase.priceNanoSatsPerToken,
+ minAcceptedTokens: testCase.acceptedTokens,
+ makerPk,
+ ...BASE_PARAMS_SLP,
+ });
+ const askedSats = agoraPartial.askedSats(testCase.acceptedTokens);
+ const requiredSats = askedSats + 2000n;
+ const [fuelInput, takerInput] = await makeBuilderInputs([
+ 4000,
+ Number(requiredSats),
+ ]);
+
+ const offer = await makeSlpOffer({
+ chronik,
+ ecc,
+ agoraPartial,
+ makerSk,
+ fuelInput,
+ });
+ const acceptTxid = await takeSlpOffer({
+ chronik,
+ ecc,
+ takerSk,
+ offer,
+ takerInput,
+ acceptedTokens: testCase.acceptedTokens,
+ });
+ const acceptTx = await chronik.tx(acceptTxid);
+ const offeredTokens = agoraPartial.offeredTokens();
+ const agora = new Agora(chronik);
+ if (testCase.acceptedTokens == offeredTokens) {
+ // FULL ACCEPT
+ // 0th output is OP_RETURN SLP SEND
+ expect(acceptTx.outputs[0].outputScript).to.equal(
+ toHex(
+ slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [
+ 0,
+ agoraPartial.offeredTokens(),
+ ]).bytecode,
+ ),
+ );
+ // 1st output is sats to maker
+ expect(acceptTx.outputs[1].token).to.equal(undefined);
+ expect(acceptTx.outputs[1].value).to.equal(testCase.askedSats);
+ expect(acceptTx.outputs[1].outputScript).to.equal(
+ makerScriptHex,
+ );
+ // 2nd output is tokens to taker
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
+ offeredTokens.toString(),
+ );
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ 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 = offeredTokens - testCase.acceptedTokens;
+ const leftoverTruncTokens =
+ leftoverTokens >> BigInt(8 * agoraPartial.numTokenTruncBytes);
+ // 0th output is OP_RETURN SLP SEND
+ expect(acceptTx.outputs[0].outputScript).to.equal(
+ toHex(
+ slpSend(agoraPartial.tokenId, agoraPartial.tokenType, [
+ 0,
+ leftoverTokens,
+ testCase.acceptedTokens,
+ ]).bytecode,
+ ),
+ );
+ // 1st output is sats to maker
+ expect(acceptTx.outputs[1].token).to.equal(undefined);
+ expect(acceptTx.outputs[1].value).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?.amount).to.equal(
+ leftoverTokens.toString(),
+ );
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
+ expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal(
+ 'a914',
+ );
+ // 3rd output is tokens to taker
+ expect(acceptTx.outputs[3].token?.amount).to.equal(
+ testCase.acceptedTokens.toString(),
+ );
+ expect(acceptTx.outputs[3].value).to.equal(DEFAULT_DUST_LIMIT);
+ 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,
+ truncTokens: leftoverTruncTokens,
+ }),
+ });
+
+ // Cancel leftover offer
+ const cancelFeeSats = newOffer.cancelFeeSats({
+ recipientScript: makerScript,
+ extraInputs: [fuelInput], // dummy input for measuring
+ });
+ const cancelTxSer = newOffer
+ .cancelTx({
+ ecc,
+ cancelSk: makerSk,
+ fuelInputs: await makeBuilderInputs([
+ Number(cancelFeeSats),
+ ]),
+ recipientScript: makerScript,
+ })
+ .ser();
+ const cancelTxid = (await chronik.broadcastTx(cancelTxSer)).txid;
+ const cancelTx = await chronik.tx(cancelTxid);
+ expect(cancelTx.outputs[1].token?.amount).to.equal(
+ leftoverTokens.toString(),
+ );
+ expect(cancelTx.outputs[1].outputScript).to.equal(makerScriptHex);
+ });
+ }
});

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 1, 10:28 (11 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5184169
Default Alt Text
D16745.diff (83 KB)

Event Timeline