-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[auto-self-nominate] Implement repeating self nomination
- Loading branch information
Showing
1 changed file
with
196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import { U64 } from "codechain-primitives/lib"; | ||
import { SDK } from "codechain-sdk"; | ||
import { PlatformAddress } from "codechain-sdk/lib/core/classes"; | ||
import { Custom } from "codechain-sdk/lib/core/transaction/Custom"; | ||
|
||
const RLP = require("rlp"); | ||
|
||
const ACTION_TAG_SELF_NOMINATE = 4; | ||
const STAKE_ACTION_HANDLER_ID = 2; | ||
const NEED_NOMINATION_UNDER_TERM_LEFT = 5; | ||
|
||
interface CandidatesInfo { | ||
[key: string]: [number, number]; | ||
} | ||
|
||
function getConfig(field: string): string { | ||
const c = process.env[field]; | ||
if (c == null) { | ||
throw new Error(`${field} is not specified`); | ||
} | ||
return c; | ||
} | ||
|
||
function rpcUrl(server: string): string { | ||
switch (server) { | ||
case "corgi": | ||
return "https://corgi-rpc.codechain.io/"; | ||
case "mainnet": | ||
return "https://rpc.codechain.io/"; | ||
default: | ||
throw Error("Invalid server configuration"); | ||
} | ||
} | ||
|
||
function networkId(server: string): string { | ||
switch (server) { | ||
case "beagle": | ||
return "bc"; | ||
case "corgi": | ||
return "wc"; | ||
case "mainnet": | ||
return "cc"; | ||
default: | ||
throw Error("Invalid server configuration"); | ||
} | ||
} | ||
|
||
const SERVER: string = (() => { | ||
const server = process.env.SERVER || "beagle"; | ||
if (["beagle", "corgi", "mainnet"].indexOf(server) >= 0) { | ||
return server; | ||
} else { | ||
throw Error("Invalid server configuration"); | ||
} | ||
})(); | ||
|
||
const sdk = (() => { | ||
console.log(`sdk ${SERVER} ${process.env.RPC_URL}`); | ||
return new SDK({ | ||
server: process.env.RPC_URL || rpcUrl(SERVER), | ||
networkId: networkId(SERVER), | ||
}); | ||
})(); | ||
|
||
function decodePlatformAddressFromPubkey(buffer: Buffer): PlatformAddress { | ||
const pubkey = buffer.toString("hex"); | ||
return PlatformAddress.fromPublic(pubkey, { | ||
networkId: sdk.networkId, | ||
}); | ||
} | ||
|
||
function decodeNumber(buffer: Buffer): number { | ||
return parseInt(buffer.toString("hex"), 16); | ||
} | ||
|
||
async function getCurrentTermId(bestBlockNumber: number): Promise<number | null> { | ||
return new Promise((resolve, reject) => { | ||
sdk.rpc | ||
.sendRpcRequest("chain_getTermMetadata", [bestBlockNumber]) | ||
.then(result => { | ||
if (result === null) { | ||
resolve(null); | ||
} | ||
const [prevTermLastBlockNumber, currentTermId] = result; | ||
if ( | ||
typeof prevTermLastBlockNumber === "number" && | ||
typeof currentTermId === "number" | ||
) { | ||
resolve(currentTermId); | ||
} | ||
reject( | ||
Error( | ||
`Expected getTermMetadata to return [number, number] | null but it returned ${result}`, | ||
), | ||
); | ||
}) | ||
.catch(reject); | ||
}); | ||
} | ||
|
||
async function sendSelfNominateTransaction( | ||
accountInfo: { account: string; passphrase: string }, | ||
params: { deposit: U64; metadata: string }, | ||
) { | ||
const tx = createSelfNomination(params); | ||
const { account, passphrase } = accountInfo; | ||
await sdk.rpc.account.sendTransaction({ | ||
tx, | ||
account, | ||
passphrase, | ||
}); | ||
} | ||
|
||
function createSelfNomination(params: { deposit: U64; metadata: string }): Custom { | ||
const { deposit, metadata } = params; | ||
const handlerId = new U64(STAKE_ACTION_HANDLER_ID); | ||
const actionTag = ACTION_TAG_SELF_NOMINATE; | ||
const bytes = RLP.encode([actionTag, deposit, metadata]); | ||
return new Custom( | ||
{ | ||
handlerId, | ||
bytes, | ||
}, | ||
networkId(SERVER), | ||
); | ||
} | ||
|
||
async function getCandidatesInfo(bestBlockNumber: number): Promise<CandidatesInfo> { | ||
const retval: CandidatesInfo = {}; | ||
const data = (await sdk.rpc.engine.getCustomActionData(2, ["Candidates"], bestBlockNumber))!; | ||
const list: Buffer[][] = RLP.decode(Buffer.from(data, "hex")); | ||
list.forEach( | ||
([encodedPubkey, encodedDeposit, encodedNominationEnd]) => | ||
(retval[decodePlatformAddressFromPubkey(encodedPubkey).value] = [ | ||
decodeNumber(encodedNominationEnd), | ||
decodeNumber(encodedDeposit), | ||
]), | ||
); | ||
return retval; | ||
} | ||
|
||
async function supplementaryNoimationDeposit( | ||
info: CandidatesInfo, | ||
bestBlockNumber: number, | ||
address: string, | ||
targetDeposit: number, | ||
): Promise<number | null> { | ||
if (!info.hasOwnProperty(address)) { | ||
throw new Error(`SelfNominate first with specific deposit vakue before repeating`); | ||
} | ||
const [deposit, nominationEndAt] = info[address]; | ||
const currentTermId = (await getCurrentTermId(bestBlockNumber))!; | ||
if (nominationEndAt - currentTermId >= NEED_NOMINATION_UNDER_TERM_LEFT) { | ||
return null; | ||
} | ||
if (deposit < targetDeposit) { | ||
return targetDeposit - deposit; | ||
} else { | ||
return 0; | ||
} | ||
} | ||
|
||
async function main() { | ||
const accountAddress = getConfig("ACCOUNT_ADDRESS"); | ||
const passphrase = getConfig("PASSPHRASE"); | ||
const metadata = getConfig("METADATA"); | ||
const targetDeposit = parseInt(getConfig("TARGET_DEPOSIT"), 10); | ||
|
||
setInterval(async () => { | ||
const bestBlockNumber = await sdk.rpc.chain.getBestBlockNumber()!; | ||
const info = await getCandidatesInfo(bestBlockNumber); | ||
const supplement = await supplementaryNoimationDeposit( | ||
info, | ||
bestBlockNumber, | ||
accountAddress, | ||
targetDeposit, | ||
); | ||
if (supplement != null) { | ||
await sendSelfNominateTransaction( | ||
{ | ||
account: accountAddress, | ||
passphrase, | ||
}, | ||
{ | ||
deposit: new U64(supplement), | ||
metadata, | ||
}, | ||
); | ||
} | ||
}, 600_000); // 10 minutes interval | ||
} | ||
|
||
main().catch(error => { | ||
console.log({ error }); | ||
throw error; | ||
}); |