Homa Liquid Staking
To interact with Acala or Karura from Javascript you can use @polkadot/api
along with @acala-network/api
.
For those looking to stake DOT or unstake LDOT, we also offer the Homa SDK.
Homa Example
The following example demonstrates how to utilize the Homa SDK and the Polkadot API for staking DOT and unstaking LDOT.
You can fork the live example on replit and run the example yourself.
To run the example on replit, you neeed to first fork it, otherwise it will only show an empty webview page.
Actual result will be logged to console instead of the webview page, only after you fork it.
prepare env, api, and sdk
first import deps
const { fetchConfig, setupWithServer } = require('@acala-network/chopsticks')
const { ApiPromise, WsProvider } = require('@polkadot/api')
const { Homa, Wallet } = require('@acala-network/sdk')
const { createTestKeyring } = require('@polkadot/keyring')
use Chopsticks to fork mainnet to a local testnet
const acalaConfig = await fetchConfig('acala')
acalaConfig.port = 8111 // set the port
acalaConfig.block = 4286000
acalaConfig.db = './db.sqlite'
const { chain, listenPort, close } = await setupWithServer(acalaConfig)
// This server can be accessed at wss://acalajs-examples--xlc.repl.co/
// Use https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Facalajs-examples--xlc.repl.co to connect to this testnet
console.log('Chopsticks is running on port', listenPort)
------------------------------------------- console -------------------------------------------
INFO (14701): Loading config file https://raw.githubusercontent.com/AcalaNetwork/chopsticks/master/configs/acala.yml
2023-08-23 14:22:26 REGISTRY: Unknown signed extensions SetEvmOrigin found, treating them as no-effect
Chopsticks is running on port 8111
INFO (rpc/14701): Acala RPC listening on port 8111
setup Acala api connect to the local testnet
const provider = new WsProvider(`ws://localhost:${listenPort}`)
const api = new ApiPromise({ provider, noInitWarn: true })
await api.isReady
add a couple helpers
// send a transaction and wait for it to be included
const sendTxAndWait = (tx) => {
return new Promise((resolve) => {
tx.send((status) => {
if (status.isInBlock || status.isFinalized) {
resolve(status.events)
}
})
})
}
// sign tx with a mock signature that will be accepted by Chopsticks
// so we can act on behalf of any accounts without the private key
// this requires mockSignatureHost to be enabled
const fakeSign = (tx, addr, nonce) => {
const mockSignature = new Uint8Array(64)
mockSignature.fill(0xcd)
mockSignature.set([0xde, 0xad, 0xbe, 0xef])
tx.signFake(addr, {
nonce,
genesisHash: api.genesisHash,
runtimeVersion: api.runtimeVersion,
blockHash: api.genesisHash,
})
// update fake signature
tx.signature.set(mockSignature)
return tx
}
setup wallet sdk and homa sdk
const wallet = new Wallet(api)
await wallet.isReady
const homa = new Homa(api, wallet)
await homa.isReady
query some info
// treasury address
const address = '23M5ttkmR6KcoTAAE6gcmibnKFtVaTP5yxnY8HF1BmrJ2A1i'
console.log('Address', address)
const ldot = { Token: 'LDOT' }
// check LDOT balance
const balance = await api.query.tokens.accounts(address, ldot)
const free = balance.free.toNumber()
console.log('LDOT Balance:', free / 10 ** 10)
// get exchange rate
const exchangeRate = (await homa.getEnv()).exchangeRate.toNumber()
console.log('Exchange Rate:', exchangeRate)
// calculate DOT amount
const dotAmount = free * exchangeRate
console.log('DOT amount:', dotAmount / 10 ** 10)
------------------------------------------- console -------------------------------------------
Address 23M5ttkmR6KcoTAAE6gcmibnKFtVaTP5yxnY8HF1BmrJ2A1i
LDOT Balance: 21.1962713105
Exchange Rate: 0.13037338530710973
DOT amount: 2.7634296466378525
stake DOT
// Bob
const testaddr = '246gNkjCexYRsCpdjtVhz35sHjcb21jpqipzT9u4uwKV8iEE'
// use Chopsticks to mint this user some ACA and DOT
await api.rpc('dev_setStorage', {
System: {
Account: [
[
// key
[testaddr],
// value
{
providers: 2, // 1 for ACA, 1 for DOT
data: {
free: 1000 * 1e12 // 1000 ACA
}
}
]
]
},
Tokens: {
Accounts: [
[
// key
[
testaddr,
{ Token: 'DOT' }
],
// value
{
free: 100 * 10e10 // 100 DOT
}
]
]
}
})
{
// create tx to use 100 DOT to mint LDOT
const tx = api.tx.homa.mint(100 * 10e10)
fakeSign(tx, testaddr, 0)
console.log('\nSend tx to use 100 DOT to mint LDOT')
const events = await sendTxAndWait(tx)
console.log('tx events', events.map(x => x.event.toHuman()))
}
const ldotBalance = (await api.query.tokens.accounts(testaddr, ldot)).free.toNumber()
console.log('\nMinted', ldotBalance / 1e10, 'LDOT')
------------------------------------------- console -------------------------------------------
Send tx to use 100 DOT to mint LDOT
INFO (block-builder/14701): Try building block #4,286,001
number: 4286001
extrinsicsCount: 1
umpCount: 0
tx events [
<many tx events>
]
Minted 7667.5245218183 LDOT
unstake LDOT
There are number of ways to unstake LDOT / convert it back to DOT
Slow unstake by wait for 28 days.
Use fast redeem by matching with new stakers. A fast redeem fee is charged. This is only available if there are DOT in the pending pool.
Use the Taiga tDOT pool to swap LDOT to DOT. A swap fee is charged. The swap rate depends on the pool balance
slow unstake
// unstake half
const unstakeAmount = Math.floor(ldotBalance / 2)
{
// create tx to unstake without fast redeem. i.e. wait for 28 days unstaking period
const tx = api.tx.homa.requestRedeem(unstakeAmount, false /* true to allow fast redeem if possible */)
fakeSign(tx, testaddr, 1)
console.log('Send tx to unstake', unstakeAmount / 1e10, 'LDOT')
const events = await sendTxAndWait(tx)
console.log('tx events', events.map(x => x.event.toHuman()))
}
// the redeem request is still pending and not yet enacted on relaychain
const pendingRedeemLDOT = (await api.query.homa.redeemRequests(testaddr))
console.log('\nPending redeem LDOT amount', pendingRedeemLDOT.toHuman())
const keyring = createTestKeyring()
const sudoKey = keyring.getPair('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') // Alice
// simulate relaychain era bump
// this will trigger XCM to do unstake on relaychain
await sendTxAndWait(await api.tx.sudo.sudo(api.tx.homa.forceBumpCurrentEra(1)).signAsync(sudoKey))
const unbondings = await api.query.homa.unbondings.entries(testaddr)
console.log('\nUnbonding requests', unbondings.map(([key, value]) => [key.toHuman(), value.toHuman()]))
// bump 28 era so that the DOT is available for withdraw
await sendTxAndWait(await api.tx.sudo.sudo(api.tx.homa.forceBumpCurrentEra(28)).signAsync(sudoKey))
await sendTxAndWait(fakeSign(api.tx.homa.claimRedemption(testaddr /* any account can trigger redeem */), testaddr, 2))
const dotBalance = (await api.query.tokens.accounts(testaddr, { token: 'DOT'})).free.toNumber()
console.log('\nRedeemed', dotBalance / 1e10, 'DOT')
------------------------------------------- console -------------------------------------------
Send tx to unstake 3833.7622609091 LDOT
INFO (block-builder/14701): Block built
number: 4286001
hash: "0xaaec1a0bc862a76af6b53bcc60c7b62256487cce5c468828360950491d281d7f"
extrinsics: [
"0xbd0184008eaf04151687736326c9fea1…cdcdcdcd00000074000b00a0724e1809"
]
pendingExtrinsicsCount: 0
ump: {}
INFO (block-builder/14701): Try building block #4,286,002
number: 4286002
extrinsicsCount: 1
umpCount: 0
tx events [
<tx events>
]
Pending redeem LDOT amount [ '38,337,622,609,091', false ]
INFO (block-builder/14701): Block built
number: 4286002
hash: "0xed20da2f91d4a5f2d3104c2c25bd3005d965a4837e0ccb90f9bba865b6b0c2fd"
extrinsics: [
"0xc10184008eaf04151687736326c9fea1…cdcdcd00040074010bc38c602cde2200"
]
pendingExtrinsicsCount: 0
ump: {}
INFO (block-builder/14701): Try building block #4,286,003
number: 4286003
extrinsicsCount: 1
umpCount: 0
Unbonding requests [
[
[ '246gNkjCexYRsCpdjtVhz35sHjcb21jpqipzT9u4uwKV8iEE', '1,205' ],
'4,999,981,935,420'
]
]
INFO (block-builder/14701): Block built
number: 4286003
hash: "0xece387f4671b9245969411be150f01958642c28b8e663c8b374dc24941e361d4"
extrinsics: [
"0xbd018400d43593c715fdd31c61141abd…6d6e0a8924010000ff00740801000000"
]
pendingExtrinsicsCount: 0
ump: {}
INFO (block-builder/14701): Try building block #4,286,004
number: 4286004
extrinsicsCount: 1
umpCount: 0
INFO (block-builder/14701): Block built
number: 4286004
hash: "0x6509c89c4bb36e7181a0d0016ec80c74bed40faa967b0179d7c149d0e0212baf"
extrinsics: [
"0xbd018400d43593c715fdd31c61141abd…e53ea68134010400ff0074081c000000"
]
pendingExtrinsicsCount: 0
ump: {}
INFO (block-builder/14701): Try building block #4,286,005
number: 4286005
extrinsicsCount: 1
umpCount: 0
Redeemed 499.998193542 DOT
swap with taiga tDOT pool
use aggregatedDex to perform swap, which allow multiple step swap with both Taiga pool and Acala Dex pool in a single tx
const tx = api.tx.aggregatedDex.swapWithExactSupply(
[{Taiga: [/*pool id*/ 0, /*supply asset*/1, /*target asset*/0]}],
unstakeAmount,
0 // should always set a min target to prevent unexpected result
)
fakeSign(tx, testaddr, 3)
console.log('Send tx to swap', unstakeAmount / 1e10, 'LDOT')
const events = await sendTxAndWait(tx)
console.log('tx events', events.map(x => x.event.toHuman()))
------------------------------------------- console -------------------------------------------
Send tx to swap 3833.7622609091 LDOT
INFO (block-builder/14701): Block built
number: 4286005
hash: "0xf62a24768622c45aca7411c77df101dffb934f2a4d63dfd19dc3ff795e008f71"
extrinsics: [
"0x210284008eaf04151687736326c9fea1…87613693c912909cb226aa4794f26a48"
]
pendingExtrinsicsCount: 0
ump: {}
INFO (block-builder/14701): Try building block #4,286,006
number: 4286006
extrinsicsCount: 1
umpCount: 0
tx events [
<tx events>
]
INFO (block-builder/14701): Block built
number: 4286006
hash: "0xcd8e6b58affca45dae23d1321cf0ff22897d0ae8782c6703bcf31ccba8a0accb"
extrinsics: [
"0xf90184008eaf04151687736326c9fea1…01000000000000000bc38c602cde2200"
]
pendingExtrinsicsCount: 0
ump: {}
clean up
shutdown the Chopsticks server
await close()
More References
Last updated