Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13115244
D16745.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
83 KB
Subscribers
None
D16745.diff
View Options
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
Details
Attached
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)
Attached To
D16745: [ecash-agora] Partial Agora offers: Add SLP support to plugin, `Agora` and `AgoraOffer`
Event Timeline
Log In to Comment