Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

Commit

Permalink
feat: add fluent.Multisig
Browse files Browse the repository at this point in the history
  • Loading branch information
kratico committed Dec 23, 2022
1 parent dcd2305 commit c696ed7
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 63 deletions.
6 changes: 2 additions & 4 deletions effects/entryRead.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as Z from "../deps/zones.ts"
import * as rpc from "../rpc/mod.ts"
import * as U from "../util/mod.ts"
import { entryReadRaw } from "./entryReadRaw.ts"
import { entryMetadata, metadata, palletMetadata } from "./metadata.ts"
import { state } from "./rpc_known_methods.ts"
import * as scale from "./scale.ts"

export function entryRead<Client extends Z.$<rpc.Client>>(client: Client) {
Expand All @@ -21,9 +21,7 @@ export function entryRead<Client extends Z.$<rpc.Client>>(client: Client) {
const deriveCodec_ = scale.deriveCodec(metadata_)
const palletMetadata_ = palletMetadata(metadata_, palletName)
const entryMetadata_ = entryMetadata(palletMetadata_, entryName)
const $storageKey_ = scale.$storageKey(deriveCodec_, palletMetadata_, entryMetadata_)
const storageKey = scale.scaleEncoded($storageKey_, Z.ls(...keys)).next(U.hex.encode)
const storageBytesHex = state.getStorage(client)(storageKey, blockHash)
const storageBytesHex = entryReadRaw(client)(palletName, entryName, keys)
const storageBytes = storageBytesHex.next(U.hex.decode)
const entryValueTypeI = entryMetadata_.access("value")
const $entry = scale.codec(deriveCodec_, entryValueTypeI)
Expand Down
28 changes: 28 additions & 0 deletions effects/entryReadRaw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as Z from "../deps/zones.ts"
import * as rpc from "../rpc/mod.ts"
import * as U from "../util/mod.ts"
import { entryMetadata, metadata, palletMetadata } from "./metadata.ts"
import { state } from "./rpc_known_methods.ts"
import * as scale from "./scale.ts"

export function entryReadRaw<Client extends Z.$<rpc.Client>>(client: Client) {
return <
PalletName extends Z.$<string>,
EntryName extends Z.$<string>,
Keys extends unknown[],
Rest extends [blockHash?: Z.$<U.HexHash | undefined>],
>(
palletName: PalletName,
entryName: EntryName,
keys: [...Keys],
...[blockHash]: [...Rest]
) => {
const metadata_ = metadata(client)(blockHash)
const deriveCodec_ = scale.deriveCodec(metadata_)
const palletMetadata_ = palletMetadata(metadata_, palletName)
const entryMetadata_ = entryMetadata(palletMetadata_, entryName)
const $storageKey_ = scale.$storageKey(deriveCodec_, palletMetadata_, entryMetadata_)
const storageKey = scale.scaleEncoded($storageKey_, Z.ls(...keys)).next(U.hex.encode)
return state.getStorage(client)(storageKey, blockHash)
}
}
7 changes: 7 additions & 0 deletions effects/extrinsic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export class Extrinsic<
},
}))
}

get callHash() {
// TODO: compute call hash
// let call = RuntimeCall::Balances(BalancesCall::transfer { dest, value }))
// let hash = blake2_256(&call);
return unimplemented()
}
}

export class SignedExtrinsic<
Expand Down
1 change: 1 addition & 0 deletions effects/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./blockWatch.ts"
export * from "./const.ts"
export * as contracts from "./contracts/mod.ts"
export * from "./entryRead.ts"
export * from "./entryReadRaw.ts"
export * from "./entryWatch.ts"
export * from "./events.ts"
export * from "./extrinsic.ts"
Expand Down
110 changes: 51 additions & 59 deletions examples/multisig_transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import * as C from "http://localhost:5646/@local/mod.ts"
import * as T from "http://localhost:5646/@local/test_util/mod.ts"
import * as U from "http://localhost:5646/@local/util/mod.ts"

import { client } from "http://localhost:5646/@local/proxy/dev:polkadot/@v0.9.31/capi.ts"
import { extrinsic } from "http://localhost:5646/@local/proxy/dev:polkadot/@v0.9.31/mod.ts"
import {
Balances,
Multisig,
System,
} from "http://localhost:5646/@local/proxy/dev:polkadot/@v0.9.31/pallets/mod.ts"
import { SignedExtrinsic } from "../effects/extrinsic.ts"

// FIXME: remove this check once the Zones .bind(env) fix is merged
const hostname = Deno.env.get("TEST_CTX_HOSTNAME")
Expand All @@ -16,85 +17,76 @@ if (!hostname || !portRaw) {
throw new Error("Must be running inside a test ctx")
}

const signatories = T.users.slice(0, 3).map((pair) => pair.publicKey)
const THRESHOLD = 2
const multisigAddress = U.multisigAddress(signatories, THRESHOLD)
const multisig = new C.fluent.Multisig(
client,
T.users.slice(0, 3).map((pair) => pair.publicKey),
2,
)

// Transfer initial balance (existential deposit) to multisig address
const existentialDeposit = extrinsic({
sender: T.alice.address,
call: Balances.transfer({
value: 2_000_000_000_000n,
dest: C.MultiAddress.Id(multisigAddress),
dest: C.MultiAddress.Id(multisig.address),
}),
})
.signed(T.alice.sign)
.watch(({ end }) => (status) => {
console.log(`Existential deposit:`, status)
if (C.rpc.known.TransactionStatus.isTerminal(status)) {
return end()
}
return
})

// The proposal
const call = Balances.transferKeepAlive({
dest: T.dave.address,
value: 1230000000000n,
})

// First approval root
const proposal = createOrApproveMultisigProposal("Proposal", T.alice)
const proposalByAlice = multisig.ratify({
sender: T.alice.address,
call,
maybeTimepoint: undefined,
})
.signed(T.alice.sign)

// Get the proposal callHash
// TODO: implement extrinsic().callHash
const callHash = multisig.proposals(1).access(0).access(1).as<Uint8Array>()

// Get the key of the timepoint
const key = Multisig.Multisigs.keys(multisigAddress).readPage(1)
.access(0)
.access(1)
// Get the timepoint
const maybeTimepoint = multisig.proposal(callHash).access("value").access("when")

// Get the timepoint itself
const maybeTimepoint = Multisig.Multisigs.entry(multisigAddress, key).read()
.access("value")
.access("when")
// Approve without executing the proposal
const voteByBob = multisig.vote({
sender: T.bob.address,
callHash,
maybeTimepoint,
})
.signed(T.bob.sign)

const approval = createOrApproveMultisigProposal("Approval", T.bob, maybeTimepoint)
// Approve and execute the proposal
const approvalByCharlie = multisig.ratify({
sender: T.charlie.address,
call,
maybeTimepoint,
})
.signed(T.charlie.sign)

// check T.dave new balance
const daveBalance = System.Account.entry(T.dave.publicKey).read()

// TODO: use common env
U.throwIfError(await existentialDeposit.run())
U.throwIfError(await proposal.run())
U.throwIfError(await approval.run())
U.throwIfError(await watchExtrinsic(existentialDeposit, "Existential deposit").run())
U.throwIfError(await watchExtrinsic(proposalByAlice, "Proposal").run())
console.log("Is proposed?", U.throwIfError(await multisig.isProposed(callHash).run()))
U.throwIfError(await watchExtrinsic(voteByBob, "Vote").run())
console.log(
"Existing approvals",
U.throwIfError(await multisig.proposal(callHash).access("value").access("approvals").run()),
)
U.throwIfError(await watchExtrinsic(approvalByCharlie, "Approval").run())
console.log(U.throwIfError(await daveBalance.run()))

function createOrApproveMultisigProposal<
Rest extends [
MaybeTimepoint?: C.Z.$<{
height: number
index: number
}>,
],
>(
label: string,
pair: U.Sr25519,
...[maybeTimepoint]: Rest
) {
const call = Balances.transferKeepAlive({
dest: T.dave.address,
value: 1230000000000n,
})
const maxWeight = extrinsic({
sender: C.MultiAddress.Id(multisigAddress),
call,
})
.feeEstimate
.access("weight")
return extrinsic({
sender: pair.address,
call: C.Z.call.fac(Multisig.asMulti, null!)(C.Z.rec({
threshold: THRESHOLD,
call,
otherSignatories: signatories.filter((value) => value !== pair.publicKey),
storeCall: false,
maxWeight,
maybeTimepoint: maybeTimepoint as Rest[0],
})),
})
.signed(pair.sign)
function watchExtrinsic(extrinsic: SignedExtrinsic, label: string) {
return extrinsic
.watch(({ end }) => (status) => {
console.log(`${label}:`, status)
if (C.rpc.known.TransactionStatus.isTerminal(status)) {
Expand Down
105 changes: 105 additions & 0 deletions fluent/Multisig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as Z from "../deps/zones.ts"
import { entryRead, entryReadRaw, keyPageRead } from "../effects/mod.ts"
import { extrinsic } from "../mod.ts"
import { MultiAddress } from "../primitives/mod.ts"
import * as rpc from "../rpc/mod.ts"
import { multisigAddress, u8a } from "../util/mod.ts"

interface Timepoint {
height: number
index: number
}

interface RatifyProps {
sender: MultiAddress
call: unknown
maybeTimepoint?: Timepoint
}

interface VoteProps {
sender: MultiAddress
callHash: Uint8Array
maybeTimepoint?: Timepoint
}

export class Multisig<Client extends Z.Effect<rpc.Client>> {
readonly address
constructor(
readonly client: Client,
readonly signatories: Uint8Array[],
readonly threshold: number,
) {
this.address = multisigAddress(signatories, threshold)
}

ratify<Props extends Z.Rec$<RatifyProps>>({
sender,
call,
maybeTimepoint,
}: Props) {
const maxWeight = extrinsic(this.client)({
sender,
call,
})
.feeEstimate
.access("weight")
return extrinsic(this.client)({
sender,
call: Z.rec({
type: "Multisig",
value: Z.rec({
type: "asMulti",
threshold: this.threshold,
call,
otherSignatories: Z.ls(sender).next(([sender]) =>
this.signatories.filter((value) => !u8a.isEqual(value, sender.value!))
),
storeCall: false,
maxWeight,
maybeTimepoint,
}),
}),
})
}

vote<Props extends Z.Rec$<VoteProps>>({
sender,
callHash,
maybeTimepoint,
}: Props) {
return extrinsic(this.client)({
sender,
call: Z.rec({
type: "Multisig",
value: Z.rec({
type: "approveAsMulti",
threshold: this.threshold,
callHash,
otherSignatories: Z.ls(sender).next(([sender]) =>
this.signatories.filter((value) => !u8a.isEqual(value, sender.value!))
),
storeCall: false,
maxWeight: {
refTime: 0n,
proofSize: 0n,
},
maybeTimepoint,
}),
}),
})
}

proposals(count: number) {
return keyPageRead(this.client)("Multisig", "Multisigs", count, [this.address])
}

proposal(callHash: Z.$<Uint8Array>) {
return entryRead(this.client)("Multisig", "Multisigs", [this.address, callHash])
}

isProposed(callHash: Z.$<Uint8Array>) {
return entryReadRaw(this.client)("Multisig", "Multisigs", [this.address, callHash]).next(
(entry: any) => entry !== null,
)
}
}
1 change: 1 addition & 0 deletions fluent/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as C from "../mod.ts"
import * as U from "../util/mod.ts"

export * from "./Contract.ts"
export * from "./Multisig.ts"

export class Storage<C extends C.Z.$<C.rpc.Client>, K extends unknown[], V> {
constructor(
Expand Down
1 change: 1 addition & 0 deletions util/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * as ss58 from "./ss58.ts"
export * from "./state.ts"
export * from "./tuple.ts"
export * from "./types.ts"
export * as u8a from "./u8a.ts"
11 changes: 11 additions & 0 deletions util/u8a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function isEqual(a: Uint8Array, b: Uint8Array) {
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false
}
}
return true
}

0 comments on commit c696ed7

Please sign in to comment.