Skip to content

Instantly share code, notes, and snippets.

@tiero
Last active May 18, 2022 10:33
Show Gist options
  • Save tiero/e4ab3bf899785ff5e55218171955421f to your computer and use it in GitHub Desktop.
Save tiero/e4ab3bf899785ff5e55218171955421f to your computer and use it in GitHub Desktop.
Spend an advanced transaction combining LDK and Marina Web Provider
import * as ecc from 'tiny-secp256k1';
import { AssetHash, confidential, networks, address, Psbt, script } from 'liquidjs-lib';
import { craftMultipleRecipientsPset, greedyCoinSelector, UnblindedOutput, AddressInterface } from 'ldk';
async function main() {
// 0. First, let's get our coins available from marina
// NOTICE: This is not yet deployed version yet 👉 https://github.com/vulpemventures/marina/issues/342
// Can be done now with LDK's `fetchAndUnblindUtxos` method
const coins = await window.marina.getCoins();
const changeAddress = await window.marina.getNextChangeAddress();
// 1. create an empty psbt object
const pset = new Psbt({ network: networks.regtest });
// 2. add a custom OP_RETURN output to psbt
pset.addOutput({
script: script.compile([script.OPS.OP_RETURN, Buffer.from('hello world', 'utf-8')]),
value: confidential.satoshiToConfidentialValue(0),
asset: AssetHash.fromHex(networks.regtest.assetHash, false).bytes,
nonce: Buffer.alloc(0),
});
// 3. add P2TR address(es) as recipient(s) to psbt
const recipients = [
{
asset: networks.regtest.assetHash,
value: 50000,
//P2TR address
address: "ert1pjapc40taf80art5zfda8tsy9pt5gq3tsuuw5r7n3arfgjexeytmqa2qrvy",
},
]
// We need to be sure if the PSBT has other inputs added by someone else before marina does the funding
const marinaInputLastIndex = pset.data.inputs.length - 1
const marinaOutputLastIndex = pset.data.outputs.length - 1;
// 4. Serialize as base64 the psbt to be passed to LDK
const tx = pset.toBase64();
// 5. Craft the transaction with multiple outputs and add fee & change output to the psbt
const unsignedTx = craftMultipleRecipientsPset({
psetBase64: tx,
unspents: (coins as UnblindedOutput[]),
recipients,
coinSelector: greedyCoinSelector(),
changeAddressByAsset: (_: string) => changeAddress.confidentialAddress,
addFee: true,
});
// deserialize and inspect the transaction
const ptx = Psbt.fromBase64(unsignedTx);
//console.log(decoded.TX.toHex());
// 6. Blind the marina change output (and unblind marina's input to do so)
// create a map input index => blinding private key
// we need this to unblind the utxo data
// add your outputs and craftMultipleRecipientsPset
const inputBlindingMap = new Map<number, Buffer>();
// here if we get -1 means before marina funding, there were no inputs, so we can safely loop over all inputs in the transasction
const marinaInputs = marinaInputLastIndex === -1 ? ptx.data.inputs : ptx.data.inputs.slice(marinaInputLastIndex + 1);
// loop over the inputs, get the blinding private key for each of them and add to the map
marinaInputs.forEach(async (input, index) => {
const blindPrivateKey = await getBlindingKeyByScript(
input.witnessUtxo.script.toString('hex')
);
inputBlindingMap.set(
index,
Buffer.from(blindPrivateKey, 'hex')
)
});
// create a map output index => blinding PUBLIC (!) key
// this is needed to blind the marina change output
const outputBlindingMap = new Map<number, Buffer>()
// we know the last output is the fee output and we need to slice the outputs before that and after the marina last index as well
const feeOutputIndex = ptx.data.outputs.length -1;
const marinaOutputs = marinaOutputLastIndex === -1 ? ptx.data.outputs : ptx.data.outputs.slice(marinaOutputLastIndex + 1, feeOutputIndex - 1);
marinaOutputs.forEach((_, index) => {
outputBlindingMap.set(
index,
// this is the blinding publick key of the change output for marina
address.fromConfidential(changeAddress.confidentialAddress).blindingKey
);
});
await ptx.blindOutputsByIndex(
Psbt.ECCKeysGenerator(ecc),
inputBlindingMap,
outputBlindingMap
);
// 7. Sign the transaction's inputs with Marina
const signedTx = await window.marina.signTransaction(ptx.toBase64());
// 8. Broadcast the transaction to the network (need to ba added to Marina)
const txid = await window.marina.broadcastTransaction(signedTx);
console.log(txid);
}
async function getBlindingKeyByScript(script: string): Promise<string> {
try {
// get addresses from marina
const addresses = await window.marina.getAddresses();
// find the address of the requested script
let found: AddressInterface | undefined;
addresses.forEach((addr: AddressInterface) => {
const currentScript = address
.toOutputScript(addr.confidentialAddress)
.toString('hex');
if (currentScript === script) {
found = addr;
}
});
if (!found) throw new Error('no blinding key for script ' + script);
return found.blindingPrivateKey;
} catch (e) {
throw e;
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment