atomiqlabs SDK
A typescript multichain client for atomiqlabs trustlesss cross-chain swaps. Enables trustless swaps between smart chains (Solana, EVM, Starknet, etc.) and bitcoin (on-chain - L1 and lightning network - L2).
Installation
npm install github:atomiqlabs/atomiq-sdk#refactor-storage
Installing chain-specific connectors
You can install only the chain-specific connectors that your project requires
npm install github:atomiqlabs/atomiq-chain-solana#add-starknet
npm install github:atomiqlabs/atomiq-chain-starknet
How to use?
Preparations
Set Solana & Starknet RPC URL to use
const solanaRpc = "https://api.mainnet-beta.solana.com";
const starknetRpc = "https://starknet-mainnet.public.blastapi.io/rpc/v0_7";
Create swapper factory, here we can pick and choose which chains we want to have supported in the SDK, ensure the "as const" keyword is used such that the typescript compiler can properly infer the types.
const Factory = new SwapperFactory<[SolanaInitializerType, StarknetInitializerType]>([SolanaInitializer, StarknetInitializer] as const);
const Tokens = Factory.Tokens;
Browser
This uses browser's Indexed DB by default
const swapper = Factory.newSwapper({
chains: {
SOLANA: {
rpcUrl: solanaRpc
},
STARKNET: {
rpcUrl: starknetRpc
}
},
bitcoinNetwork: BitcoinNetwork.TESTNET
});
NodeJS
For NodeJS we need to explicitly use filesystem storage
const swapper = Factory.newSwapper({
chains: {
SOLANA: {
rpcUrl: solanaRpc
},
STARKNET: {
rpcUrl: starknetRpc
}
},
bitcoinNetwork: BitcoinNetwork.TESTNET,
storageCtor: (name: string) => new FileSystemStorageManager(name)
});
Signer
const anchorWallet = useAnchorWallet();
const wallet = new SolanaSigner(anchorWallet);
const swo = await connect();
const wallet = new StarknetSigner(new WalletAccount(starknetRpc, swo));
or
const wallet = swapper.randomSigner<"SOLANA">("SOLANA");
Initialization
Initialize the swapper
await swapper.init();
Now we have the multichain swapper initialized
To make it easier to do swaps between bitcoin and a specific chain we can extract a chain-specific swapper, and also set a signer.
const solanaSwapper = swapper.withChain<"SOLANA">("SOLANA").withSigner(signer);
Bitcoin on-chain swaps
Swap Smart chain -> Bitcoin on-chain
Initiating & executing the swap.
const _exactIn = false;
const _amount = 10000n;
const _address = "bc1qtw67hj77rt8zrkkg3jgngutu0yfgt9czjwusxt";
const swap = await solanaSwapper.create(
Tokens.SOLANA.SOL,
Tokens.BITCOIN.BTC,
_amount,
_exactIn,
_address
);
const amountToBePaid: string = swap.getInput().amount;
const fee: string = swap.getFee().amountInSrcToken.amount;
const expiry: number = swap.getExpiry();
await swap.commit();
const result: boolean = await swap.waitForPayment();
if(!result) {
await swap.refund();
} else {
const bitcoinTxId = swap.getBitcoinTxId();
}
Swap states
- ToBTCSwapState.REFUNDED = -3
- Swap failed and was successfully refunded
- ToBTCSwapState.QUOTE_EXPIRED = -2
- Swap quote expired and cannot be executed anymore
- ToBTCSwapState.QUOTE_SOFT_EXPIRED = -1
- Swap quote soft-expired (i.e. the quote probably expired, but if there is already an initialization transaction sent it might still succeed)
- ToBTCSwapState.CREATED = 0
- Swap quote is created, waiting to be executed
- ToBTCSwapState.COMMITED = 1,
- Swap was initiated (init transaction sent)
- ToBTCSwapState.SOFT_CLAIMED = 2,
- Swap was processed by the counterparty but not yet claimed on-chain (bitcoin transaction was sent, but unconfirmed yet)
- ToBTCSwapState.CLAIMED = 3
- Swap was finished and funds were successfully claimed by the counterparty
- ToBTCSwapState.REFUNDABLE = 4
- Swap was initiated but counterparty failed to process it, the user can now refund his funds
Swap Bitcoin on-chain -> Smart chain
Initiating & executing the swap.
const _exactIn = true;
const _amount = fromHumanReadableString("0.0001", Tokens.BITCOIN.BTC);
const swap = await solanaSwapper.create(
Tokens.BITCOIN.BTC,
Tokens.SOLANA.SOL,
_amount,
_exactIn
);
const amountToBePaidOnBitcoin: string = swap.getInput().amount;
const amountToBeReceivedOnSolana: string = swap.getOutput().amount;
const fee: string = swap.getFee().amountInSrcToken.amount;
const expiry: number = swap.getExpiry();
const securityDeposit: string = swap.getSecurityDeposit().amount;
const claimerBounty: string = swap.getClaimerBounty().amount;
await swap.commit();
const receivingAddressOnBitcoin = swap.getAddress();
const qrCodeData = swap.getQrData();
const expiryTime = swap.getTimeoutTime();
try {
await swap.waitForPayment(
null, null,
(
txId: string,
confirmations: number,
targetConfirmations: number,
transactionETAms: number
) => {
}
);
} catch(e) {
return;
}
try {
await swap.waitTillClaimed(timeoutSignal(30*1000));
} catch (e) {
await swap.claim();
}
Swap states
- FromBTCSwapState.EXPIRED = -3
- Bitcoin swap address expired
- FromBTCSwapState.QUOTE_EXPIRED = -2
- Swap quote expired and cannot be executed anymore
- FromBTCSwapState.QUOTE_SOFT_EXPIRED = -1
- Swap quote soft-expired (i.e. the quote probably expired, but if there is already an initialization transaction sent it might still succeed)
- FromBTCSwapState.PR_CREATED = 0
- Swap quote is created, waiting for the user to open a bitcoin swap address
- FromBTCSwapState.CLAIM_COMMITED = 1
- Bitcoin swap address is opened
- FromBTCSwapState.BTC_TX_CONFIRMED = 2
- Bitcoin transaction sending funds to the swap address is confirmed
- FromBTCSwapState.CLAIM_CLAIMED = 3
- Swap funds are claimed to the user's wallet
Bitcoin lightning network swaps
Swap Smart chain -> Bitcoin lightning network
const _lightningInvoice = "lnbc10u1pj2q0g9pp5ejs6m677m39cznpzum7muruvh50ys93ln82p4j9ks2luqm56xxlshp52r2anlhddfa9ex9vpw9gstxujff8a0p8s3pzvua930js0kwfea6scqzzsxqyz5vqsp5073zskc5qfgp7lre0t6s8uexxxey80ax564hsjklfwfjq2ew0ewq9qyyssqvzmgs6f8mvuwgfa9uqxhtza07qem4yfhn9wwlpskccmuwplsqmh8pdy6c42kqdu8p73kky9lsnl40qha5396d8lpgn90y27ltfc5rfqqq59cya";
const swap = await solanaSwapper.create(
Tokens.SOLANA.SOL,
Tokens.BITCOIN.BTCLN,
null,
false,
_lightningInvoice
);
const amountToBePaid: string = swap.getInput().amount;
const fee: string = swap.getFee().amountInSrcToken.amount;
const expiry: number = swap.getExpiry();
await swap.commit();
const result: boolean = await swap.waitForPayment();
if(!result) {
await swap.refund();
} else {
const lightningSecret = swap.getSecret();
}
Swap states
- ToBTCSwapState.REFUNDED = -3
- Swap failed and was successfully refunded
- ToBTCSwapState.QUOTE_EXPIRED = -2
- Swap quote expired and cannot be executed anymore
- ToBTCSwapState.QUOTE_SOFT_EXPIRED = -1
- Swap quote soft-expired (i.e. the quote probably expired, but if there is already an initialization transaction sent it might still succeed)
- ToBTCSwapState.CREATED = 0
- Swap quote is created, waiting to be executed
- ToBTCSwapState.COMMITED = 1,
- Swap was initiated (init transaction sent)
- ToBTCSwapState.SOFT_CLAIMED = 2,
- Swap was processed by the counterparty but not yet claimed on-chain (lightning network payment secret was revealed)
- ToBTCSwapState.CLAIMED = 3
- Swap was finished and funds were successfully claimed by the counterparty
- ToBTCSwapState.REFUNDABLE = 4
- Swap was initiated but counterparty failed to process it, the user can now refund his funds
Swap Bitcoin lightning network -> Smart chain
const _exactIn = true;
const _amount = 10000n;
const swap = await solanaSwapper.create(
Tokens.BITCOIN.BTCLN,
Tokens.SOLANA.SOL,
_amount,
_exactIn
);
const receivingLightningInvoice: string = swap.getLightningInvoice();
const qrCodeData: string = swap.getQrData();
const amountToBePaidOnBitcoin: string = swap.getInput().amount;
const amountToBeReceivedOnSolana: string = swap.getOutput().amount;
const fee: string = swap.getFee().amountInSrcToken.amount;
try {
await swap.waitForPayment();
await swap.commitAndClaim();
} catch(e) {
}
Swap states
- FromBTCLNSwapState.FAILED = -4
- If the claiming of the funds was initiated, but never concluded, the user will get his lightning network payment refunded
- FromBTCLNSwapState.QUOTE_EXPIRED = -3
- Swap quote expired and cannot be executed anymore
- FromBTCLNSwapState.QUOTE_SOFT_EXPIRED = -2
- Swap quote soft-expired (i.e. the quote probably expired, but if there is already an initialization transaction sent it might still succeed)
- FromBTCLNSwapState.EXPIRED = -1
- Lightning network invoice expired, meaning the swap is expired
- FromBTCLNSwapState.PR_CREATED = 0
- Swap is created, the user should now pay the provided lightning network invoice
- FromBTCLNSwapState.PR_PAID = 1
- Lightning network invoice payment was received (but cannot be settled by the counterparty yet)
- FromBTCLNSwapState.CLAIM_COMMITED = 2
- Claiming of the funds was initiated
- FromBTCLNSwapState.CLAIM_CLAIMED = 3
- Funds were successfully claimed & lightning network secret pre-image revealed, so the lightning network payment will settle now
Getting state of the swap
You can get the current state of the swap with:
const state = swap.getState();
You can also set a listener to listen for swap state changes:
swap.events.on("swapState", swap => {
const newState = swap.getState();
});
For the meaning of the states please refer to the "Swap state" section under each swap type.
LNURLs & readable lightning identifiers
LNURLs extend the lightning network functionality by creating static lightning addreses (LNURL-pay & static internet identifiers) and QR codes which allow you to pull funds from them (LNURL-withdraw)
This SDK supports:
Differences
Lightning invoices:
- One time use only
- Need to have a fixed amount, therefore recipient has to set the amount
- Static and bounded expiration
- You can only pay to a lightning invoice, not withdraw funds from it
LNURLs & lightning identifiers:
- Reusable
- Programmable expiry
- Allows payer to set an amount
- Supports both, paying (LNURL-pay) and withdrawing (LNURL-withdraw)
- Possibility to attach a message/comment to a payment
- Receive a message/url as a result of the payment
Helpers
It is good practice to automatically distinguish between lightning network invoices & LNURLs and adjust the UI accordingly.
Therefore there are a few helper functions to help with that:
const isLNInvoice: boolean = swapper.isValidLightningInvoice(_input);
const isLNURL: boolean = swapper.isValidLNURL(_input);
if(isLNURL) {
const result: (LNURLPay | LNURLWithdraw | null) = await swapper.getLNURLTypeAndData(_input);
if(result.type==="pay") {
const lnurlPayData: LNURLPay = result;
const minPayable: bigint = lnurlPayData.min;
const maxPayable: bigint = lnurlPayData.max;
const icon: (string | null) = lnurlPayData.icon;
const shortDescription: (string | null) = lnurlPayData.shortDescription;
const longDescription: (string | null) = lnurlPayData.longDescription;
const maxCommentLength: (number | 0) = lnurlPayData.commentMaxLength;
}
if(result.type==="withdraw") {
const lnurlWithdrawData: LNURLWithdraw = result;
const minWithdrawable: bigint = lnurlWithdrawData.min;
const maxWithdrawable: bigint = lnurlWithdrawData.max;
}
}
Swap Smart chain -> Bitcoin lightning network
const _lnurlOrIdentifier: string = "lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkx6rfvdjx2ctvxyesuk0a27";
const _exactIn = false;
const _amount: bigint = 10000n;
const swap = await solanaSwapper.create(
Tokens.SOLANA.SOL,
Tokens.BITCOIN.BTCLN,
_amount,
_exactIn,
_lnurlOrIdentifier
);
const amountToBePaid: string = swap.getInput().amount;
const fee: string = swap.getFee().amountInSrcToken.amount;
const expiry: number = swap.getExpiry();
await swap.commit();
const result: boolean = await swap.waitForPayment();
if(!result) {
await swap.refund();
} else {
const lightningSecret = swap.getSecret();
if(swap.hasSuccessAction()) {
const successMessage = swap.getSuccessAction();
const description: string = successMessage.description;
const text: (string | null) = successMessage.text;
const url: (string | null) = successMessage.url;
}
}
Swap Bitcoin lightning network -> Smart chain
const _lnurl: string = "lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkx6rfvdjx2ctvxyesuk0a27";
const _exactIn = true;
const _amount = 10000n;
const swap = await solanaSwapper.create(
Tokens.BITCOIN.BTCLN,
Tokens.SOLANA.SOL,
_amount,
_exactIn,
_lnurl
);
const amountToBeWithdrawnOnBitcoin: string = swap.getInput().amount;
const amountToBeReceivedOnSolana: string = swap.getOutput().amount;
const fee: string = swap.getFee().amountInSrcToken.amount;
try {
await swap.waitForPayment();
await swap.commitAndClaim();
} catch(e) {
}
Get refundable swaps
You can refund the swaps in one of two cases:
- In case intermediary is non-cooperative and goes offline, you can claim the funds from the swap contract back after some time.
- In case intermediary tried to pay but was unsuccessful, so he sent you signed message with which you can refund now without waiting.
This call can be checked on every startup and periodically every few minutes.
const refundableSwaps = await solanaSwapper.getRefundableSwaps();
for(let swap of refundableSwaps) {
await swap.refund();
}
Get claimable swaps
Returns swaps that are ready to be claimed by the client, this can happen if client closes the application when a swap is in-progress and the swap is concluded while the client is offline.
const claimableSwaps = await solanaSwapper.getClaimableSwaps();
for(let swap of claimableSwaps) {
if(swap.canCommit()) await swap.commit();
await swap.claim();
}