mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-13 11:09:23 +01:00
docs: add example for PSBT batch funding
This commit is contained in:
parent
57253c0c05
commit
bf60b20def
1 changed files with 225 additions and 0 deletions
225
docs/psbt.md
225
docs/psbt.md
|
@ -640,3 +640,228 @@ lingering reservations/intents/pending channels are cleaned up.
|
|||
|
||||
**NOTE**: You must be connected to each of the nodes you want to open channels
|
||||
to before you run the command.
|
||||
|
||||
### Example Node.JS script
|
||||
|
||||
To demonstrate how the PSBT funding API can be used with JavaScript, we add a
|
||||
simple example script that imitates the behavior of `lncli` but **does not
|
||||
publish** the final transaction itself. This allows the app creator to publish
|
||||
the transaction whenever everything is ready.
|
||||
|
||||
> multi-channel-funding.js
|
||||
```js
|
||||
const fs = require('fs');
|
||||
const grpc = require('@grpc/grpc-js');
|
||||
const protoLoader = require('@grpc/proto-loader');
|
||||
const Buffer = require('safe-buffer').Buffer;
|
||||
const randomBytes = require('random-bytes').sync;
|
||||
const prompt = require('prompt');
|
||||
|
||||
const LND_DIR = '/home/myuser/.lnd';
|
||||
const LND_HOST = 'localhost:10009';
|
||||
const NETWORK = 'regtest';
|
||||
const LNRPC_PROTO_DIR = '/home/myuser/projects/go/lnd/lnrpc';
|
||||
|
||||
const grpcOptions = {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
includeDirs: [LNRPC_PROTO_DIR],
|
||||
};
|
||||
|
||||
const packageDefinition = protoLoader.loadSync(`${LNRPC_PROTO_DIR}/rpc.proto`, grpcOptions);
|
||||
const lnrpc = grpc.loadPackageDefinition(packageDefinition).lnrpc;
|
||||
|
||||
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA';
|
||||
|
||||
const adminMac = fs.readFileSync(`${LND_DIR}/data/chain/bitcoin/${NETWORK}/admin.macaroon`);
|
||||
const metadata = new grpc.Metadata();
|
||||
metadata.add('macaroon', adminMac.toString('hex'));
|
||||
const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => {
|
||||
callback(null, metadata);
|
||||
});
|
||||
|
||||
const lndCert = fs.readFileSync(`${LND_DIR}/tls.cert`);
|
||||
const sslCreds = grpc.credentials.createSsl(lndCert);
|
||||
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);
|
||||
|
||||
const client = new lnrpc.Lightning(LND_HOST, credentials);
|
||||
|
||||
const params = process.argv.slice(2);
|
||||
|
||||
if (params.length % 2 !== 0) {
|
||||
console.log('Usage: node multi-channel-funding.js pubkey amount [pubkey amount]...')
|
||||
}
|
||||
|
||||
const channels = [];
|
||||
for (let i = 0; i < params.length; i += 2) {
|
||||
channels.push({
|
||||
pubKey: Buffer.from(params[i], 'hex'),
|
||||
amount: parseInt(params[i + 1], 10),
|
||||
pendingChanID: randomBytes(32),
|
||||
outputAddr: '',
|
||||
finalized: false,
|
||||
chanPending: null,
|
||||
cleanedUp: false,
|
||||
});
|
||||
}
|
||||
|
||||
channels.forEach(c => {
|
||||
const openChannelMsg = {
|
||||
node_pubkey: c.pubKey,
|
||||
local_funding_amount: c.amount,
|
||||
funding_shim: {
|
||||
psbt_shim: {
|
||||
pending_chan_id: c.pendingChanID,
|
||||
no_publish: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
const openChannelCall = client.OpenChannel(openChannelMsg);
|
||||
openChannelCall.on('data', function (update) {
|
||||
if (update.psbt_fund && update.psbt_fund.funding_address) {
|
||||
console.log('Got funding addr for PSBT: ' + update.psbt_fund.funding_address);
|
||||
c.outputAddr = update.psbt_fund.funding_address;
|
||||
maybeFundPSBT();
|
||||
}
|
||||
if (update.chan_pending) {
|
||||
c.chanPending = update.chan_pending;
|
||||
const txidStr = update.chan_pending.txid.reverse().toString('hex');
|
||||
console.log(`
|
||||
Channels are now pending!
|
||||
Expected TXID of published final transaction: ${txidStr}
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
openChannelCall.on('error', function (e) {
|
||||
console.log('Error on open channel call: ' + e);
|
||||
tryCleanup();
|
||||
});
|
||||
});
|
||||
|
||||
function tryCleanup() {
|
||||
function maybeExit() {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
if (!channels[i].cleanedUp) {
|
||||
// Not all channels are cleaned up yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
channels.forEach(c => {
|
||||
if (c.cleanedUp) {
|
||||
return;
|
||||
}
|
||||
if (c.chanPending === null) {
|
||||
console.log("Cleaning up channel, shim cancel")
|
||||
// The channel never made it into the pending state, let's try to
|
||||
// remove the funding shim. This is best effort. Depending on the
|
||||
// state of the channel this might fail so we don't log any errors
|
||||
// here.
|
||||
client.FundingStateStep({
|
||||
shim_cancel: {
|
||||
pending_chan_id: c.pendingChanID,
|
||||
}
|
||||
}, () => {
|
||||
c.cleanedUp = true;
|
||||
maybeExit();
|
||||
});
|
||||
} else {
|
||||
// The channel is pending but since we aborted will never make it
|
||||
// to be confirmed. We need to tell lnd to abandon this channel
|
||||
// otherwise it will show in the pending channels for forever.
|
||||
console.log("Cleaning up channel, abandon channel")
|
||||
client.AbandonChannel({
|
||||
channel_point: {
|
||||
funding_txid: {
|
||||
funding_txid_bytes: c.chanPending.txid,
|
||||
},
|
||||
output_index: c.chanPending.output_index,
|
||||
},
|
||||
i_know_what_i_am_doing: true,
|
||||
}, () => {
|
||||
c.cleanedUp = true;
|
||||
maybeExit();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function maybeFundPSBT() {
|
||||
const outputsBitcoind = [];
|
||||
const outputsLnd = {};
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const c = channels[i];
|
||||
if (c.outputAddr === '') {
|
||||
// Not all channels did get a funding address yet.
|
||||
return;
|
||||
}
|
||||
|
||||
outputsBitcoind.push({
|
||||
[c.outputAddr]: c.amount / 100000000,
|
||||
});
|
||||
outputsLnd[c.outputAddr] = c.amount;
|
||||
}
|
||||
|
||||
console.log(`
|
||||
Channels ready for funding transaction.
|
||||
Please create a funded PSBT now.
|
||||
Examples:
|
||||
|
||||
bitcoind:
|
||||
bitcoin-cli walletcreatefundedpsbt '[]' '${JSON.stringify(outputsBitcoind)}' 0 '{"fee_rate": 15}'
|
||||
|
||||
lnd:
|
||||
lncli wallet psbt fund --outputs='${JSON.stringify(outputsLnd)}' --sat_per_vbyte=15
|
||||
`);
|
||||
|
||||
prompt.get([{name: 'funded_psbt'}], (err, result) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
|
||||
tryCleanup();
|
||||
return;
|
||||
}
|
||||
channels.forEach(c => {
|
||||
const verifyMsg = {
|
||||
psbt_verify: {
|
||||
funded_psbt: Buffer.from(result.funded_psbt, 'base64'),
|
||||
pending_chan_id: c.pendingChanID,
|
||||
skip_finalize: true
|
||||
}
|
||||
};
|
||||
client.FundingStateStep(verifyMsg, (err, res) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
|
||||
tryCleanup();
|
||||
return;
|
||||
}
|
||||
if (res) {
|
||||
c.finalized = true;
|
||||
maybePublishPSBT();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function maybePublishPSBT() {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const c = channels[i];
|
||||
if (!channels[i].finalized) {
|
||||
// Not all channels are verified/finalized yet.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`
|
||||
PSBT verification successful!
|
||||
You can now sign and publish the transaction.
|
||||
Make sure the TXID does not change!
|
||||
`);
|
||||
}
|
||||
```
|
||||
|
|
Loading…
Add table
Reference in a new issue