@argonprotocol/mainchain 1.3.18 → 1.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/index.d.ts +93 -69
- package/browser/index.js +301 -289
- package/browser/index.js.map +1 -1
- package/lib/index.cjs +302 -290
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +93 -69
- package/lib/index.d.ts +93 -69
- package/lib/index.js +302 -290
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -562,8 +562,7 @@ var Vault = class _Vault {
|
|
|
562
562
|
baseFee,
|
|
563
563
|
bitcoinXpub,
|
|
564
564
|
tip,
|
|
565
|
-
doNotExceedBalance
|
|
566
|
-
txProgressCallback
|
|
565
|
+
doNotExceedBalance
|
|
567
566
|
} = args;
|
|
568
567
|
let xpubBytes = hexToU8a(bitcoinXpub);
|
|
569
568
|
if (xpubBytes.length !== 78) {
|
|
@@ -606,7 +605,6 @@ var Vault = class _Vault {
|
|
|
606
605
|
...args,
|
|
607
606
|
useLatestNonce: true
|
|
608
607
|
});
|
|
609
|
-
const tickDuration = config.tickDurationMillis ?? await client.query.ticks.genesisTicker().then((x) => x.tickDurationMillis.toNumber());
|
|
610
608
|
async function getVault() {
|
|
611
609
|
await result.waitForFinalizedBlock;
|
|
612
610
|
let vaultId;
|
|
@@ -619,11 +617,7 @@ var Vault = class _Vault {
|
|
|
619
617
|
if (vaultId === void 0) {
|
|
620
618
|
throw new Error("Vault creation failed, no VaultCreated event found");
|
|
621
619
|
}
|
|
622
|
-
|
|
623
|
-
if (rawVault.isNone) {
|
|
624
|
-
throw new Error("Vault creation failed, vault not found");
|
|
625
|
-
}
|
|
626
|
-
return new _Vault(vaultId, rawVault.unwrap(), tickDuration);
|
|
620
|
+
return _Vault.get(client, vaultId, config.tickDurationMillis);
|
|
627
621
|
}
|
|
628
622
|
return { getVault, txResult: result };
|
|
629
623
|
}
|
|
@@ -645,25 +639,280 @@ function fromFixedNumber(value, decimals = FIXED_U128_DECIMALS) {
|
|
|
645
639
|
var FIXED_U128_DECIMALS = 18;
|
|
646
640
|
var PERMILL_DECIMALS = 6;
|
|
647
641
|
|
|
648
|
-
// src/
|
|
642
|
+
// src/BitcoinLock.ts
|
|
649
643
|
import { u8aToHex } from "@polkadot/util";
|
|
650
644
|
var SATS_PER_BTC = 100000000n;
|
|
651
|
-
var
|
|
652
|
-
|
|
653
|
-
|
|
645
|
+
var BitcoinLock = class _BitcoinLock {
|
|
646
|
+
utxoId;
|
|
647
|
+
p2wshScriptHashHex;
|
|
648
|
+
vaultId;
|
|
649
|
+
peggedPrice;
|
|
650
|
+
liquidityPromised;
|
|
651
|
+
ownerAccount;
|
|
652
|
+
satoshis;
|
|
653
|
+
vaultPubkey;
|
|
654
|
+
securityFees;
|
|
655
|
+
vaultClaimPubkey;
|
|
656
|
+
ownerPubkey;
|
|
657
|
+
vaultXpubSources;
|
|
658
|
+
vaultClaimHeight;
|
|
659
|
+
openClaimHeight;
|
|
660
|
+
createdAtHeight;
|
|
661
|
+
isVerified;
|
|
662
|
+
isRejectedNeedsRelease;
|
|
663
|
+
fundHoldExtensionsByBitcoinExpirationHeight;
|
|
664
|
+
constructor(data) {
|
|
665
|
+
this.utxoId = data.utxoId;
|
|
666
|
+
this.p2wshScriptHashHex = data.p2wshScriptHashHex;
|
|
667
|
+
this.vaultId = data.vaultId;
|
|
668
|
+
this.peggedPrice = data.peggedPrice;
|
|
669
|
+
this.liquidityPromised = data.liquidityPromised;
|
|
670
|
+
this.ownerAccount = data.ownerAccount;
|
|
671
|
+
this.satoshis = data.satoshis;
|
|
672
|
+
this.vaultPubkey = data.vaultPubkey;
|
|
673
|
+
this.securityFees = data.securityFees;
|
|
674
|
+
this.vaultClaimPubkey = data.vaultClaimPubkey;
|
|
675
|
+
this.ownerPubkey = data.ownerPubkey;
|
|
676
|
+
this.vaultXpubSources = data.vaultXpubSources;
|
|
677
|
+
this.vaultClaimHeight = data.vaultClaimHeight;
|
|
678
|
+
this.openClaimHeight = data.openClaimHeight;
|
|
679
|
+
this.createdAtHeight = data.createdAtHeight;
|
|
680
|
+
this.isVerified = data.isVerified;
|
|
681
|
+
this.isRejectedNeedsRelease = data.isRejectedNeedsRelease;
|
|
682
|
+
this.fundHoldExtensionsByBitcoinExpirationHeight = data.fundHoldExtensionsByBitcoinExpirationHeight;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Gets the UTXO reference by ID.
|
|
686
|
+
* @param client - client at the block height to query the UTXO reference at a specific point in time.
|
|
687
|
+
* @return An object containing the transaction ID and output index, or undefined if not found.
|
|
688
|
+
* @return.txid - The Bitcoin transaction ID of the UTXO.
|
|
689
|
+
* @return.vout - The output index of the UTXO in the transaction.
|
|
690
|
+
* @return.bitcoinTxid - The Bitcoin transaction ID of the UTXO formatted in little endian
|
|
691
|
+
*/
|
|
692
|
+
async getUtxoRef(client) {
|
|
693
|
+
const refRaw = await client.query.bitcoinUtxos.utxoIdToRef(this.utxoId);
|
|
694
|
+
if (!refRaw) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const ref = refRaw.unwrap();
|
|
698
|
+
const txid = u8aToHex(ref.txid);
|
|
699
|
+
const bitcoinTxid = u8aToHex(ref.txid.reverse());
|
|
700
|
+
const vout = ref.outputIndex.toNumber();
|
|
701
|
+
return { txid, vout, bitcoinTxid };
|
|
702
|
+
}
|
|
703
|
+
async findPendingMints(client) {
|
|
704
|
+
const pendingMint = await client.query.mint.pendingMintUtxos();
|
|
705
|
+
const mintsPending = [];
|
|
706
|
+
for (const [utxoIdRaw, _accountId, mintAmountRaw] of pendingMint) {
|
|
707
|
+
if (utxoIdRaw.toNumber() === this.utxoId) {
|
|
708
|
+
mintsPending.push(mintAmountRaw.toBigInt());
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return mintsPending;
|
|
712
|
+
}
|
|
713
|
+
async getRatchetPrice(client, priceIndex, vault) {
|
|
714
|
+
const { createdAtHeight, vaultClaimHeight, peggedPrice, satoshis } = this;
|
|
715
|
+
const marketRate = await _BitcoinLock.getMarketRate(priceIndex, BigInt(satoshis));
|
|
716
|
+
let ratchetingFee = vault.terms.bitcoinBaseFee;
|
|
717
|
+
let burnAmount = 0n;
|
|
718
|
+
if (marketRate > peggedPrice) {
|
|
719
|
+
const lockFee = vault.calculateBitcoinFee(marketRate);
|
|
720
|
+
const currentBitcoinHeight = await client.query.bitcoinUtxos.confirmedBitcoinBlockTip().then((x) => x.unwrap().blockHeight.toNumber());
|
|
721
|
+
const blockLength = vaultClaimHeight - createdAtHeight;
|
|
722
|
+
const elapsed = (currentBitcoinHeight - createdAtHeight) / blockLength;
|
|
723
|
+
const remainingDuration = 1 - elapsed;
|
|
724
|
+
ratchetingFee = BigInt(remainingDuration * Number(lockFee));
|
|
725
|
+
} else {
|
|
726
|
+
burnAmount = await this.releasePrice(priceIndex);
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
ratchetingFee,
|
|
730
|
+
burnAmount,
|
|
731
|
+
marketRate
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async ratchet(args) {
|
|
735
|
+
const { priceIndex, argonKeyring, tip = 0n, vault, client } = args;
|
|
736
|
+
const ratchetPrice = await this.getRatchetPrice(client, priceIndex, vault);
|
|
737
|
+
const txSubmitter = new TxSubmitter(
|
|
738
|
+
client,
|
|
739
|
+
client.tx.bitcoinLocks.ratchet(this.utxoId),
|
|
740
|
+
argonKeyring
|
|
741
|
+
);
|
|
742
|
+
const canAfford = await txSubmitter.canAfford({
|
|
743
|
+
tip,
|
|
744
|
+
unavailableBalance: BigInt(ratchetPrice.burnAmount + ratchetPrice.ratchetingFee)
|
|
745
|
+
});
|
|
746
|
+
if (!canAfford.canAfford) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
`Insufficient funds to ratchet lock. Available: ${formatArgons(canAfford.availableBalance)}, Required: ${formatArgons(
|
|
749
|
+
ratchetPrice.burnAmount + ratchetPrice.ratchetingFee
|
|
750
|
+
)}`
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const txResult = await txSubmitter.submit(args);
|
|
754
|
+
const getRatchetResult = async () => {
|
|
755
|
+
const blockHash = await txResult.waitForFinalizedBlock;
|
|
756
|
+
const ratchetEvent = txResult.events.find(
|
|
757
|
+
(x) => client.events.bitcoinLocks.BitcoinLockRatcheted.is(x)
|
|
758
|
+
);
|
|
759
|
+
if (!ratchetEvent) {
|
|
760
|
+
throw new Error(`Ratchet event not found in transaction events`);
|
|
761
|
+
}
|
|
762
|
+
const api = await client.at(blockHash);
|
|
763
|
+
const bitcoinBlockHeight = await api.query.bitcoinUtxos.confirmedBitcoinBlockTip().then((x) => x.unwrap().blockHeight.toNumber());
|
|
764
|
+
const {
|
|
765
|
+
amountBurned,
|
|
766
|
+
liquidityPromised: liquidityPromisedRaw,
|
|
767
|
+
newPeggedPrice,
|
|
768
|
+
originalPeggedPrice,
|
|
769
|
+
securityFee
|
|
770
|
+
} = ratchetEvent.data;
|
|
771
|
+
const liquidityPromised = liquidityPromisedRaw.toBigInt();
|
|
772
|
+
let mintAmount = liquidityPromised;
|
|
773
|
+
if (liquidityPromised > originalPeggedPrice.toBigInt()) {
|
|
774
|
+
mintAmount -= originalPeggedPrice.toBigInt();
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
txFee: txResult.finalFee ?? 0n,
|
|
778
|
+
blockHeight: txResult.blockNumber,
|
|
779
|
+
bitcoinBlockHeight,
|
|
780
|
+
pendingMint: mintAmount,
|
|
781
|
+
liquidityPromised,
|
|
782
|
+
newPeggedPrice: newPeggedPrice.toBigInt(),
|
|
783
|
+
burned: amountBurned.toBigInt(),
|
|
784
|
+
securityFee: securityFee.toBigInt()
|
|
785
|
+
};
|
|
786
|
+
};
|
|
787
|
+
return {
|
|
788
|
+
txResult,
|
|
789
|
+
getRatchetResult
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async releasePrice(priceIndex) {
|
|
793
|
+
return await _BitcoinLock.getRedemptionRate(priceIndex, this);
|
|
654
794
|
}
|
|
655
|
-
async
|
|
795
|
+
async requestRelease(args) {
|
|
796
|
+
const {
|
|
797
|
+
priceIndex,
|
|
798
|
+
releaseRequest: { bitcoinNetworkFee, toScriptPubkey },
|
|
799
|
+
argonKeyring,
|
|
800
|
+
tip = 0n,
|
|
801
|
+
client
|
|
802
|
+
} = args;
|
|
803
|
+
if (!toScriptPubkey.startsWith("0x")) {
|
|
804
|
+
throw new Error("toScriptPubkey must be a hex string starting with 0x");
|
|
805
|
+
}
|
|
806
|
+
const submitter = new TxSubmitter(
|
|
807
|
+
client,
|
|
808
|
+
client.tx.bitcoinLocks.requestRelease(this.utxoId, toScriptPubkey, bitcoinNetworkFee),
|
|
809
|
+
argonKeyring
|
|
810
|
+
);
|
|
811
|
+
const redemptionPrice = await _BitcoinLock.getRedemptionRate(priceIndex, this);
|
|
812
|
+
const canAfford = await submitter.canAfford({
|
|
813
|
+
tip,
|
|
814
|
+
unavailableBalance: BigInt(redemptionPrice)
|
|
815
|
+
});
|
|
816
|
+
if (!canAfford.canAfford) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`Insufficient funds to release lock. Available: ${formatArgons(canAfford.availableBalance)}, Required: ${formatArgons(redemptionPrice + canAfford.txFee + tip)}`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return submitter.submit({
|
|
822
|
+
logResults: true,
|
|
823
|
+
...args
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async getReleaseRequest(client) {
|
|
827
|
+
const requestMaybe = await client.query.bitcoinLocks.lockReleaseRequestsByUtxoId(this.utxoId);
|
|
828
|
+
if (!requestMaybe.isSome) {
|
|
829
|
+
return void 0;
|
|
830
|
+
}
|
|
831
|
+
const request = requestMaybe.unwrap();
|
|
832
|
+
return {
|
|
833
|
+
toScriptPubkey: request.toScriptPubkey.toHex(),
|
|
834
|
+
bitcoinNetworkFee: request.bitcoinNetworkFee.toBigInt(),
|
|
835
|
+
dueFrame: request.cosignDueFrame.toNumber(),
|
|
836
|
+
vaultId: request.vaultId.toNumber(),
|
|
837
|
+
redemptionPrice: request.redemptionPrice.toBigInt()
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Finds the cosign signature for a vault lock by UTXO ID. Optionally waits for the signature
|
|
842
|
+
* @param client - The Argon client with rpc access
|
|
843
|
+
* @param finalizedStateOnly - If true, only checks finalized state
|
|
844
|
+
* @param waitForSignatureMillis - Optional timeout in milliseconds to wait for the signature. If -1, waits indefinitely.
|
|
845
|
+
*/
|
|
846
|
+
async findVaultCosignSignature(client, finalizedStateOnly = false, waitForSignatureMillis) {
|
|
847
|
+
let queryClient = client;
|
|
848
|
+
if (finalizedStateOnly) {
|
|
849
|
+
const finalizedHead = await client.rpc.chain.getFinalizedHead();
|
|
850
|
+
queryClient = await client.at(finalizedHead);
|
|
851
|
+
}
|
|
852
|
+
const releaseHeight = await queryClient.query.bitcoinLocks.lockReleaseCosignHeightById(
|
|
853
|
+
this.utxoId
|
|
854
|
+
);
|
|
855
|
+
if (releaseHeight.isSome) {
|
|
856
|
+
const releaseHeightValue = releaseHeight.unwrap().toNumber();
|
|
857
|
+
const signature = await this.getVaultCosignSignature(client, releaseHeightValue);
|
|
858
|
+
if (signature) {
|
|
859
|
+
return { blockHeight: releaseHeightValue, signature };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!waitForSignatureMillis) {
|
|
863
|
+
return void 0;
|
|
864
|
+
}
|
|
865
|
+
return await new Promise(async (resolve, reject) => {
|
|
866
|
+
let timeout;
|
|
867
|
+
const unsub = await client.rpc.chain.subscribeNewHeads((header) => {
|
|
868
|
+
const atHeight = header.number.toNumber();
|
|
869
|
+
this.getVaultCosignSignature(client, atHeight).then((signature) => {
|
|
870
|
+
if (signature) {
|
|
871
|
+
unsub?.();
|
|
872
|
+
clearTimeout(timeout);
|
|
873
|
+
resolve({ signature, blockHeight: atHeight });
|
|
874
|
+
}
|
|
875
|
+
}).catch((err) => {
|
|
876
|
+
console.error(`Error checking for cosign signature at height ${atHeight}:`, err);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
if (waitForSignatureMillis !== -1) {
|
|
880
|
+
timeout = setTimeout(() => {
|
|
881
|
+
unsub?.();
|
|
882
|
+
reject(new Error(`Timeout waiting for cosign signature for UTXO ID ${this.utxoId}`));
|
|
883
|
+
}, waitForSignatureMillis);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async getVaultCosignSignature(client, atHeight) {
|
|
888
|
+
const blockHash = await _BitcoinLock.blockHashAtHeight(client, atHeight);
|
|
889
|
+
if (!blockHash) {
|
|
890
|
+
console.warn(`Block hash not found for height ${atHeight}`);
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
const blockEvents = await client.at(blockHash).then((api) => api.query.system.events());
|
|
894
|
+
for (const event of blockEvents) {
|
|
895
|
+
if (client.events.bitcoinLocks.BitcoinUtxoCosigned.is(event.event)) {
|
|
896
|
+
const { utxoId: id, signature } = event.event.data;
|
|
897
|
+
if (id.toNumber() === this.utxoId) {
|
|
898
|
+
return new Uint8Array(signature);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return void 0;
|
|
903
|
+
}
|
|
904
|
+
static async getUtxoIdFromEvents(client, events) {
|
|
656
905
|
for (const event of events) {
|
|
657
|
-
if (
|
|
906
|
+
if (client.events.bitcoinLocks.BitcoinLockCreated.is(event)) {
|
|
658
907
|
return event.data.utxoId.toNumber();
|
|
659
908
|
}
|
|
660
909
|
}
|
|
661
910
|
return void 0;
|
|
662
911
|
}
|
|
663
|
-
async getMarketRate(priceIndex, satoshis) {
|
|
912
|
+
static async getMarketRate(priceIndex, satoshis) {
|
|
664
913
|
return priceIndex.getBtcMicrogonPrice(satoshis);
|
|
665
914
|
}
|
|
666
|
-
async getRedemptionRate(priceIndex, details) {
|
|
915
|
+
static async getRedemptionRate(priceIndex, details) {
|
|
667
916
|
const { satoshis, peggedPrice } = details;
|
|
668
917
|
const satsPerArgon = Number(SATS_PER_BTC) / MICROGONS_PER_ARGON;
|
|
669
918
|
let price = Number(priceIndex.btcUsdPrice);
|
|
@@ -684,28 +933,7 @@ var BitcoinLocks = class {
|
|
|
684
933
|
}
|
|
685
934
|
return BigInt(Math.floor(price * multiplier));
|
|
686
935
|
}
|
|
687
|
-
async
|
|
688
|
-
const client = this.client;
|
|
689
|
-
const sats = client.createType("U64", satoshis.toString());
|
|
690
|
-
const marketRate = await client.rpc.state.call("BitcoinApis_market_rate", sats.toHex(true));
|
|
691
|
-
const rate = client.createType("Option<U128>", marketRate);
|
|
692
|
-
if (!rate.isSome) {
|
|
693
|
-
throw new Error("Market rate not available");
|
|
694
|
-
}
|
|
695
|
-
return rate.value.toBigInt();
|
|
696
|
-
}
|
|
697
|
-
async getRedemptionRateApi(satoshis) {
|
|
698
|
-
const client = this.client;
|
|
699
|
-
const sats = client.createType("U64", satoshis.toString());
|
|
700
|
-
const marketRate = await client.rpc.state.call("BitcoinApis_redemption_rate", sats.toHex(true));
|
|
701
|
-
const rate = client.createType("Option<U128>", marketRate);
|
|
702
|
-
if (!rate.isSome) {
|
|
703
|
-
throw new Error("Redemption rate not available");
|
|
704
|
-
}
|
|
705
|
-
return rate.value.toBigInt();
|
|
706
|
-
}
|
|
707
|
-
async getConfig() {
|
|
708
|
-
const client = this.client;
|
|
936
|
+
static async getConfig(client) {
|
|
709
937
|
const bitcoinNetwork = await client.query.bitcoinUtxos.bitcoinNetwork();
|
|
710
938
|
return {
|
|
711
939
|
lockReleaseCosignDeadlineFrames: client.consts.bitcoinLocks.lockReleaseCosignDeadlineFrames.toNumber(),
|
|
@@ -714,48 +942,11 @@ var BitcoinLocks = class {
|
|
|
714
942
|
bitcoinNetwork
|
|
715
943
|
};
|
|
716
944
|
}
|
|
717
|
-
async getBitcoinConfirmedBlockHeight() {
|
|
718
|
-
return await
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Gets the UTXO reference by ID.
|
|
722
|
-
* @param utxoId - The UTXO ID to look up.
|
|
723
|
-
* @param clientAtHeight - Optional client at the block height to query the UTXO reference at a specific point in time.
|
|
724
|
-
* @return An object containing the transaction ID and output index, or undefined if not found.
|
|
725
|
-
* @return.txid - The Bitcoin transaction ID of the UTXO.
|
|
726
|
-
* @return.vout - The output index of the UTXO in the transaction.
|
|
727
|
-
* @return.bitcoinTxid - The Bitcoin transaction ID of the UTXO formatted in little endian
|
|
728
|
-
*/
|
|
729
|
-
async getUtxoRef(utxoId, clientAtHeight) {
|
|
730
|
-
const client = clientAtHeight ?? this.client;
|
|
731
|
-
const refRaw = await client.query.bitcoinUtxos.utxoIdToRef(utxoId);
|
|
732
|
-
if (!refRaw) {
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const ref = refRaw.unwrap();
|
|
736
|
-
const txid = u8aToHex(ref.txid);
|
|
737
|
-
const bitcoinTxid = u8aToHex(ref.txid.reverse());
|
|
738
|
-
const vout = ref.outputIndex.toNumber();
|
|
739
|
-
return { txid, vout, bitcoinTxid };
|
|
740
|
-
}
|
|
741
|
-
async getReleaseRequest(utxoId, clientAtHeight) {
|
|
742
|
-
const client = clientAtHeight ?? this.client;
|
|
743
|
-
const requestMaybe = await client.query.bitcoinLocks.lockReleaseRequestsByUtxoId(utxoId);
|
|
744
|
-
if (!requestMaybe.isSome) {
|
|
745
|
-
return void 0;
|
|
746
|
-
}
|
|
747
|
-
const request = requestMaybe.unwrap();
|
|
748
|
-
return {
|
|
749
|
-
toScriptPubkey: request.toScriptPubkey.toHex(),
|
|
750
|
-
bitcoinNetworkFee: request.bitcoinNetworkFee.toBigInt(),
|
|
751
|
-
dueFrame: request.cosignDueFrame.toNumber(),
|
|
752
|
-
vaultId: request.vaultId.toNumber(),
|
|
753
|
-
redemptionPrice: request.redemptionPrice.toBigInt()
|
|
754
|
-
};
|
|
945
|
+
static async getBitcoinConfirmedBlockHeight(client) {
|
|
946
|
+
return await client.query.bitcoinUtxos.confirmedBitcoinBlockTip().then((x) => x.value?.blockHeight.toNumber() ?? 0);
|
|
755
947
|
}
|
|
756
|
-
async submitVaultSignature(args) {
|
|
757
|
-
const { utxoId, vaultSignature, argonKeyring,
|
|
758
|
-
const client = this.client;
|
|
948
|
+
static async submitVaultSignature(args) {
|
|
949
|
+
const { utxoId, vaultSignature, argonKeyring, client } = args;
|
|
759
950
|
if (!vaultSignature || vaultSignature.byteLength < 70 || vaultSignature.byteLength > 73) {
|
|
760
951
|
throw new Error(
|
|
761
952
|
`Invalid vault signature length: ${vaultSignature.byteLength}. Must be 70-73 bytes.`
|
|
@@ -766,8 +957,8 @@ var BitcoinLocks = class {
|
|
|
766
957
|
const submitter = new TxSubmitter(client, tx, argonKeyring);
|
|
767
958
|
return await submitter.submit(args);
|
|
768
959
|
}
|
|
769
|
-
async
|
|
770
|
-
const utxoRaw = await
|
|
960
|
+
static async get(client, utxoId) {
|
|
961
|
+
const utxoRaw = await client.query.bitcoinLocks.locksByUtxoId(utxoId);
|
|
771
962
|
if (!utxoRaw.isSome) {
|
|
772
963
|
return;
|
|
773
964
|
}
|
|
@@ -798,7 +989,7 @@ var BitcoinLocks = class {
|
|
|
798
989
|
const fundHoldExtensionsByBitcoinExpirationHeight = Object.fromEntries(
|
|
799
990
|
[...utxo.fundHoldExtensions.entries()].map(([x, y]) => [x.toNumber(), y.toBigInt()])
|
|
800
991
|
);
|
|
801
|
-
return {
|
|
992
|
+
return new _BitcoinLock({
|
|
802
993
|
utxoId,
|
|
803
994
|
p2wshScriptHashHex,
|
|
804
995
|
vaultId,
|
|
@@ -817,50 +1008,9 @@ var BitcoinLocks = class {
|
|
|
817
1008
|
isVerified,
|
|
818
1009
|
isRejectedNeedsRelease,
|
|
819
1010
|
fundHoldExtensionsByBitcoinExpirationHeight
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Finds the cosign signature for a vault lock by UTXO ID. Optionally waits for the signature
|
|
824
|
-
* @param utxoId - The UTXO ID of the bitcoin lock
|
|
825
|
-
* @param waitForSignatureMillis - Optional timeout in milliseconds to wait for the signature. If -1, waits indefinitely.
|
|
826
|
-
*/
|
|
827
|
-
async findVaultCosignSignature(utxoId, waitForSignatureMillis) {
|
|
828
|
-
const client = this.client;
|
|
829
|
-
const releaseHeight = await client.query.bitcoinLocks.lockReleaseCosignHeightById(utxoId);
|
|
830
|
-
if (releaseHeight.isSome) {
|
|
831
|
-
const releaseHeightValue = releaseHeight.unwrap().toNumber();
|
|
832
|
-
const signature = await this.getVaultCosignSignature(utxoId, releaseHeightValue);
|
|
833
|
-
if (signature) {
|
|
834
|
-
return { blockHeight: releaseHeightValue, signature };
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
if (!waitForSignatureMillis) {
|
|
838
|
-
return void 0;
|
|
839
|
-
}
|
|
840
|
-
return await new Promise(async (resolve, reject) => {
|
|
841
|
-
let timeout;
|
|
842
|
-
const unsub = await client.rpc.chain.subscribeNewHeads((header) => {
|
|
843
|
-
const atHeight = header.number.toNumber();
|
|
844
|
-
this.getVaultCosignSignature(utxoId, atHeight).then((signature) => {
|
|
845
|
-
if (signature) {
|
|
846
|
-
unsub?.();
|
|
847
|
-
clearTimeout(timeout);
|
|
848
|
-
resolve({ signature, blockHeight: atHeight });
|
|
849
|
-
}
|
|
850
|
-
}).catch((err) => {
|
|
851
|
-
console.error(`Error checking for cosign signature at height ${atHeight}:`, err);
|
|
852
|
-
});
|
|
853
|
-
});
|
|
854
|
-
if (waitForSignatureMillis !== -1) {
|
|
855
|
-
timeout = setTimeout(() => {
|
|
856
|
-
unsub?.();
|
|
857
|
-
reject(new Error(`Timeout waiting for cosign signature for UTXO ID ${utxoId}`));
|
|
858
|
-
}, waitForSignatureMillis);
|
|
859
|
-
}
|
|
860
1011
|
});
|
|
861
1012
|
}
|
|
862
|
-
async blockHashAtHeight(atHeight) {
|
|
863
|
-
const client = this.client;
|
|
1013
|
+
static async blockHashAtHeight(client, atHeight) {
|
|
864
1014
|
for (let i = 0; i < 10; i++) {
|
|
865
1015
|
const currentHeight = await client.query.system.number().then((x) => x.toNumber());
|
|
866
1016
|
if (atHeight > currentHeight) {
|
|
@@ -880,37 +1030,16 @@ var BitcoinLocks = class {
|
|
|
880
1030
|
}
|
|
881
1031
|
return void 0;
|
|
882
1032
|
}
|
|
883
|
-
async
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const { utxoId: id, signature } = event.event.data;
|
|
894
|
-
if (id.toNumber() === utxoId) {
|
|
895
|
-
return new Uint8Array(signature);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
return void 0;
|
|
900
|
-
}
|
|
901
|
-
async findPendingMints(utxoId) {
|
|
902
|
-
const pendingMint = await this.client.query.mint.pendingMintUtxos();
|
|
903
|
-
const mintsPending = [];
|
|
904
|
-
for (const [utxoIdRaw, _accountId, mintAmountRaw] of pendingMint) {
|
|
905
|
-
if (utxoIdRaw.toNumber() === utxoId) {
|
|
906
|
-
mintsPending.push(mintAmountRaw.toBigInt());
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return mintsPending;
|
|
910
|
-
}
|
|
911
|
-
async createInitializeLockTx(args) {
|
|
912
|
-
const { vault, priceIndex, argonKeyring, satoshis, tip = 0n, ownerBitcoinPubkey } = args;
|
|
913
|
-
const client = this.client;
|
|
1033
|
+
static async createInitializeTx(args) {
|
|
1034
|
+
const {
|
|
1035
|
+
vault,
|
|
1036
|
+
priceIndex,
|
|
1037
|
+
argonKeyring,
|
|
1038
|
+
satoshis,
|
|
1039
|
+
tip = 0n,
|
|
1040
|
+
ownerBitcoinPubkey,
|
|
1041
|
+
client
|
|
1042
|
+
} = args;
|
|
914
1043
|
if (ownerBitcoinPubkey.length !== 33) {
|
|
915
1044
|
throw new Error(
|
|
916
1045
|
`Invalid Bitcoin key length: ${ownerBitcoinPubkey.length}. Must be a compressed pukey (33 bytes).`
|
|
@@ -932,23 +1061,9 @@ var BitcoinLocks = class {
|
|
|
932
1061
|
});
|
|
933
1062
|
return { tx, securityFee, txFee, canAfford, availableBalance, txFeePlusTip: txFee + tip };
|
|
934
1063
|
}
|
|
935
|
-
async
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
const utxoId = await this.getUtxoIdFromEvents(txResult.events) ?? 0;
|
|
939
|
-
if (utxoId === 0) {
|
|
940
|
-
throw new Error("Bitcoin lock creation failed, no UTXO ID found in transaction events");
|
|
941
|
-
}
|
|
942
|
-
const lock = await this.getBitcoinLock(utxoId);
|
|
943
|
-
if (!lock) {
|
|
944
|
-
throw new Error(`Lock with ID ${utxoId} not found after initialization`);
|
|
945
|
-
}
|
|
946
|
-
return { lock, createdAtHeight: blockHeight, txResult };
|
|
947
|
-
}
|
|
948
|
-
async initializeLock(args) {
|
|
949
|
-
const { argonKeyring } = args;
|
|
950
|
-
const client = this.client;
|
|
951
|
-
const { tx, securityFee, canAfford, txFeePlusTip } = await this.createInitializeLockTx(args);
|
|
1064
|
+
static async initialize(args) {
|
|
1065
|
+
const { argonKeyring, client } = args;
|
|
1066
|
+
const { tx, securityFee, canAfford, txFeePlusTip } = await this.createInitializeTx(args);
|
|
952
1067
|
if (!canAfford) {
|
|
953
1068
|
throw new Error(
|
|
954
1069
|
`Insufficient funds to initialize bitcoin lock. Required security fee: ${formatArgons(securityFee)}, Tx fee plus tip: ${formatArgons(txFeePlusTip)}`
|
|
@@ -957,130 +1072,27 @@ var BitcoinLocks = class {
|
|
|
957
1072
|
const submitter = new TxSubmitter(client, tx, argonKeyring);
|
|
958
1073
|
const txResult = await submitter.submit({ logResults: true, ...args });
|
|
959
1074
|
return {
|
|
960
|
-
getLock: () => this.getBitcoinLockFromTxResult(txResult),
|
|
1075
|
+
getLock: () => this.getBitcoinLockFromTxResult(client, txResult),
|
|
961
1076
|
txResult,
|
|
962
1077
|
securityFee
|
|
963
1078
|
};
|
|
964
1079
|
}
|
|
965
|
-
async
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
const {
|
|
972
|
-
lock,
|
|
973
|
-
priceIndex,
|
|
974
|
-
releaseRequest: { bitcoinNetworkFee, toScriptPubkey },
|
|
975
|
-
argonKeyring,
|
|
976
|
-
tip = 0n
|
|
977
|
-
} = args;
|
|
978
|
-
if (!toScriptPubkey.startsWith("0x")) {
|
|
979
|
-
throw new Error("toScriptPubkey must be a hex string starting with 0x");
|
|
980
|
-
}
|
|
981
|
-
const submitter = new TxSubmitter(
|
|
982
|
-
client,
|
|
983
|
-
client.tx.bitcoinLocks.requestRelease(lock.utxoId, toScriptPubkey, bitcoinNetworkFee),
|
|
984
|
-
argonKeyring
|
|
985
|
-
);
|
|
986
|
-
const redemptionPrice = await this.getRedemptionRate(priceIndex, lock);
|
|
987
|
-
const canAfford = await submitter.canAfford({
|
|
988
|
-
tip,
|
|
989
|
-
unavailableBalance: BigInt(redemptionPrice)
|
|
990
|
-
});
|
|
991
|
-
if (!canAfford.canAfford) {
|
|
992
|
-
throw new Error(
|
|
993
|
-
`Insufficient funds to release lock. Available: ${formatArgons(canAfford.availableBalance)}, Required: ${formatArgons(redemptionPrice + canAfford.txFee + tip)}`
|
|
994
|
-
);
|
|
1080
|
+
static async getBitcoinLockFromTxResult(client, txResult) {
|
|
1081
|
+
await txResult.waitForFinalizedBlock;
|
|
1082
|
+
const blockHeight = txResult.blockNumber;
|
|
1083
|
+
const utxoId = await this.getUtxoIdFromEvents(client, txResult.events) ?? 0;
|
|
1084
|
+
if (utxoId === 0) {
|
|
1085
|
+
throw new Error("Bitcoin lock creation failed, no UTXO ID found in transaction events");
|
|
995
1086
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
async releasePrice(priceIndex, lock) {
|
|
1002
|
-
return await this.getRedemptionRate(priceIndex, lock);
|
|
1003
|
-
}
|
|
1004
|
-
async getRatchetPrice(lock, priceIndex, vault) {
|
|
1005
|
-
const { createdAtHeight, vaultClaimHeight, peggedPrice, satoshis } = lock;
|
|
1006
|
-
const client = this.client;
|
|
1007
|
-
const marketRate = await this.getMarketRate(priceIndex, BigInt(satoshis));
|
|
1008
|
-
let ratchetingFee = vault.terms.bitcoinBaseFee;
|
|
1009
|
-
let burnAmount = 0n;
|
|
1010
|
-
if (marketRate > peggedPrice) {
|
|
1011
|
-
const lockFee = vault.calculateBitcoinFee(marketRate);
|
|
1012
|
-
const currentBitcoinHeight = await client.query.bitcoinUtxos.confirmedBitcoinBlockTip().then((x) => x.unwrap().blockHeight.toNumber());
|
|
1013
|
-
const blockLength = vaultClaimHeight - createdAtHeight;
|
|
1014
|
-
const elapsed = (currentBitcoinHeight - createdAtHeight) / blockLength;
|
|
1015
|
-
const remainingDuration = 1 - elapsed;
|
|
1016
|
-
ratchetingFee = BigInt(remainingDuration * Number(lockFee));
|
|
1017
|
-
} else {
|
|
1018
|
-
burnAmount = await this.releasePrice(priceIndex, lock);
|
|
1087
|
+
const lock = await this.get(client, utxoId);
|
|
1088
|
+
if (!lock) {
|
|
1089
|
+
throw new Error(`Lock with ID ${utxoId} not found after initialization`);
|
|
1019
1090
|
}
|
|
1020
|
-
return {
|
|
1021
|
-
ratchetingFee,
|
|
1022
|
-
burnAmount,
|
|
1023
|
-
marketRate
|
|
1024
|
-
};
|
|
1091
|
+
return { lock, createdAtHeight: blockHeight, txResult };
|
|
1025
1092
|
}
|
|
1026
|
-
async
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
const ratchetPrice = await this.getRatchetPrice(lock, priceIndex, vault);
|
|
1030
|
-
const txSubmitter = new TxSubmitter(
|
|
1031
|
-
client,
|
|
1032
|
-
client.tx.bitcoinLocks.ratchet(lock.utxoId),
|
|
1033
|
-
argonKeyring
|
|
1034
|
-
);
|
|
1035
|
-
const canAfford = await txSubmitter.canAfford({
|
|
1036
|
-
tip,
|
|
1037
|
-
unavailableBalance: BigInt(ratchetPrice.burnAmount + ratchetPrice.ratchetingFee)
|
|
1038
|
-
});
|
|
1039
|
-
if (!canAfford.canAfford) {
|
|
1040
|
-
throw new Error(
|
|
1041
|
-
`Insufficient funds to ratchet lock. Available: ${formatArgons(canAfford.availableBalance)}, Required: ${formatArgons(
|
|
1042
|
-
ratchetPrice.burnAmount + ratchetPrice.ratchetingFee
|
|
1043
|
-
)}`
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
const txResult = await txSubmitter.submit(args);
|
|
1047
|
-
const getRatchetResult = async () => {
|
|
1048
|
-
const blockHash = await txResult.waitForFinalizedBlock;
|
|
1049
|
-
const ratchetEvent = txResult.events.find(
|
|
1050
|
-
(x) => client.events.bitcoinLocks.BitcoinLockRatcheted.is(x)
|
|
1051
|
-
);
|
|
1052
|
-
if (!ratchetEvent) {
|
|
1053
|
-
throw new Error(`Ratchet event not found in transaction events`);
|
|
1054
|
-
}
|
|
1055
|
-
const api = await client.at(blockHash);
|
|
1056
|
-
const bitcoinBlockHeight = await api.query.bitcoinUtxos.confirmedBitcoinBlockTip().then((x) => x.unwrap().blockHeight.toNumber());
|
|
1057
|
-
const {
|
|
1058
|
-
amountBurned,
|
|
1059
|
-
liquidityPromised: liquidityPromisedRaw,
|
|
1060
|
-
newPeggedPrice,
|
|
1061
|
-
originalPeggedPrice,
|
|
1062
|
-
securityFee
|
|
1063
|
-
} = ratchetEvent.data;
|
|
1064
|
-
const liquidityPromised = liquidityPromisedRaw.toBigInt();
|
|
1065
|
-
let mintAmount = liquidityPromised;
|
|
1066
|
-
if (liquidityPromised > originalPeggedPrice.toBigInt()) {
|
|
1067
|
-
mintAmount -= originalPeggedPrice.toBigInt();
|
|
1068
|
-
}
|
|
1069
|
-
return {
|
|
1070
|
-
txFee: txResult.finalFee ?? 0n,
|
|
1071
|
-
blockHeight: txResult.blockNumber,
|
|
1072
|
-
bitcoinBlockHeight,
|
|
1073
|
-
pendingMint: mintAmount,
|
|
1074
|
-
liquidityPromised,
|
|
1075
|
-
newPeggedPrice: newPeggedPrice.toBigInt(),
|
|
1076
|
-
burned: amountBurned.toBigInt(),
|
|
1077
|
-
securityFee: securityFee.toBigInt()
|
|
1078
|
-
};
|
|
1079
|
-
};
|
|
1080
|
-
return {
|
|
1081
|
-
txResult,
|
|
1082
|
-
getRatchetResult
|
|
1083
|
-
};
|
|
1093
|
+
static async requiredSatoshisForArgonLiquidity(priceIndex, argonAmount) {
|
|
1094
|
+
const marketRatePerBitcoin = priceIndex.getBtcMicrogonPrice(SATS_PER_BTC);
|
|
1095
|
+
return argonAmount * SATS_PER_BTC / marketRatePerBitcoin;
|
|
1084
1096
|
}
|
|
1085
1097
|
};
|
|
1086
1098
|
|
|
@@ -1183,7 +1195,7 @@ async function getClient(host, options) {
|
|
|
1183
1195
|
}
|
|
1184
1196
|
export {
|
|
1185
1197
|
BTreeMap,
|
|
1186
|
-
|
|
1198
|
+
BitcoinLock,
|
|
1187
1199
|
Bool,
|
|
1188
1200
|
Bytes,
|
|
1189
1201
|
Compact,
|