How to handle effect webhook calls
In this example we will implement simple HTTP server in Node.js express which will handle the call to the webhook url defined in an effect registered previously.
We will receive an event when balance is received to a wallet as a result of successfully cleared intent which contains a claim targeting the wallet (either issue
or transfer
claim). Ledger will raise an event with balance-received
signal for each wallet which receives the balance. Event will be forwarded by the registered effect to our HTTP server with event body containing:
- handle - unique identifier of the event, used for idempotency
- signal - signal of the event, string
- amount - amount of balance received
- wallet - wallet record which received the balance
- symbol - symbol record of the received balance
- intent - intent record which caused balance change
{
"hash": "<event-hash>",
"data": {
"handle": "evt_vOcNMfFXQq3T4G6rz",
"signal": "balance-received",
"amount": 100,
"wallet": {
"hash": "<wallet-hash>",
"data": {
"handle": "bank1",
...
},
"meta": { ... }
},
"symbol": { ... },
"intent": { ... }
},
"meta": { ... }
}
We will verify the event payload and execute side-effect in external system. This side-effect can, for example, execute the transaction in banking core, send email to user, or even send new intent to the Ledger. We will use handle for idempotency check to ensure that the same event is not already processed (it can happen with retries mechanism if the ledger didn’t receive success response due to network error).
import express from 'express'
const app = express()
const port = 3000
const BANK_NAME = 'bank1'
const SYMBOL_NAME = 'usd'
const SIGNAL_BALANCE_RECEIVED = 'balance-received'
const sdk = new LedgerSdk({
server: '<your ledger URL>',
signer: {
format: 'ed25519-raw',
public: '<your ledger public key>'
}
})
/**
* LedgerSdk.proofs returns a new instance of a
* verification client which can be used to verify
* proofs coming from the ledger. By default it
* verifies that records have at least 1 proof, by
* calling the ledger() method, we also verify that
* the response is signed by the expected ledger key
*/
const verificationClient = sdk.proofs.ledger()
app.post('/balance-received', async (req, res) => {
// 1. Acknowledge the event by sendng HTTP 202 (ACCEPTED) in response
console.log(`Event received, acknowledging..`)
res.status(202).send()
// 2. Verify that event is signed by expected ledger key
console.log(`Verifying event...`)
const event = req.body
await verificationClient.verify(event)
// 3. Idempotency chech, verify that event is not already processed
if (isEventProcessed(event?.data?.handle)) {
// Event already processed
return
}
// 4. Verify that signal, symbol and wallet are correct
const signal = event?.data?.signal
if (signal !== SIGNAL_BALANCE_RECEIVED) {
// Wrong signal sent
console.error(`Wrong signal '${signal}', expected '${SIGNAL_BALANCE_RECEIVED}'`)
return
}
const walletHandle = event?.data?.wallet?.data?.handle
if (walletHandle !== BANK_NAME) {
// Wrong wallet
console.error(`Wrong target wallet '${walletHandle}', expected '${BANK_NAME}'`)
return
}
const symbolHandle = event?.data?.symbol?.data?.handle
if (symbolHandle !== SYMBOL_NAME) {
// Wrong symbol
console.error(`Wrong symbol '${symbolHandle}', expected '${SYMBOL_NAME}'`)
return
}
// 5. Process the intent which caused balance movement
await coreProcessIntent(event?.data?.intent)
})
app.listen(port, () => {
console.log(`Server running on port ${port}`)
})
Webhook handler should acknowledge that event is delivered as soon as possible by responding with a success HTTP status code (2XX) and perform internal processing after that. Every other response status code including the network error will be considered by Ledger as failed delivery and it will retry it with exponential backoff, or stop retrying only in case of the response status code being 501 Not Implemented
. The exponential backoff uses a 20% delay step increase (1s, 1.2s, 1.44s, etc., with a maximum of 1 hour) until a confirmation in the shape of a success status code is received in the http response.
Webhook handler should verify that the event payload is signed with a known ledger key. It is a proof that it originates from this ledger instance and is not changed in transport. For that the handler should keep the public key of the ledger and use it for verification as shown in code snippet.
const sdk = new LedgerSdk({
server: '<your ledger URL>',
signer: {
format: 'ed25519-raw',
public: '<your ledger public key>'
}
})
const verificationClient = sdk.proofs.ledger()
await verificationClient.verify(event)