Skip to content

Commit

Permalink
feat: sapi sign ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
0xKheops committed Aug 27, 2024
1 parent 16282f1 commit dde349f
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const InlineStakingReview = () => {
)}
{payload && (
<SapiSendButton
containerId="inlineStakingModalDialog"
label={t("Stake")}
payload={payload}
onSubmitted={onSubmitted}
Expand Down
65 changes: 53 additions & 12 deletions apps/extension/src/ui/domains/Transactions/SapiSendButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AlertCircleIcon } from "@talismn/icons"
import { toHex } from "@talismn/scale"
import { AccountType, SignerPayloadJSON } from "extension-core"
import { log } from "extension-shared"
import { FC, Suspense, useCallback, useMemo, useState } from "react"
Expand All @@ -9,17 +10,66 @@ import { Hex } from "viem"
import { useScaleApi } from "@ui/hooks/sapi/useScaleApi"
import { useAccountByAddress } from "@ui/hooks/useAccountByAddress"

import { SignHardwareSubstrate } from "../Sign/SignHardwareSubstrate"

type SapiSendButtonProps = {
containerId?: string
label?: string
payload: SignerPayloadJSON
txMetadata?: Uint8Array
disabled?: boolean // true while estimating fee
onSubmitted: (hash: Hex) => void
}

const HardwareAccountSendButton: FC<SapiSendButtonProps> = () => {
// TODO
return null
const HardwareAccountSendButton: FC<SapiSendButtonProps> = ({
containerId,
payload,
txMetadata,
onSubmitted,
}) => {
const [error, setError] = useState<string>()
const { data: sapi } = useScaleApi(payload?.genesisHash)
const shortMetadata = useMemo(() => (txMetadata ? toHex(txMetadata) : undefined), [txMetadata])

const registry = useMemo(() => {
if (!sapi) return undefined
return sapi.getTypeRegistry(payload)
}, [payload, sapi])

const handleSigned = useCallback(
async ({ signature }: { signature: Hex }) => {
if (!payload || !signature || !sapi) return

setError(undefined)
try {
const { hash } = await sapi.submit(payload, signature)
onSubmitted(hash)
} catch (err) {
log.error("Failed to submit", { payload, err })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setError((err as any)?.message ?? "Failed to submit")
}
},
[onSubmitted, payload, sapi]
)

return (
<div className="flex w-full flex-col gap-6">
{error && (
<div className="text-alert-warn bg-grey-900 flex w-full items-center gap-5 rounded-sm px-5 py-6 text-xs">
<AlertCircleIcon className="text-lg" />
<div>{error}</div>
</div>
)}
<SignHardwareSubstrate
containerId={containerId}
payload={payload}
shortMetadata={shortMetadata}
registry={registry}
onSigned={handleSigned}
/>
</div>
)
}

const QrAccountSendButton: FC<SapiSendButtonProps> = () => {
Expand Down Expand Up @@ -98,15 +148,6 @@ export const SapiSendButton: FC<SapiSendButtonProps> = (props) => {

return (
<Suspense fallback={null}>
{/* <div className="flex w-full flex-col gap-6"> */}
{/* {sendErrorMessage ? (
<div className="text-alert-warn bg-grey-900 flex w-full items-center gap-5 rounded-sm px-5 py-6 text-xs">
<AlertCircleIcon className="text-lg" />
<div>{sendErrorMessage}</div>
</div>
) : (
<ExternalRecipientWarning />
)} */}
{signMethod === "local" && <LocalAccountSendButton {...props} />}
{signMethod === "hardware" && <HardwareAccountSendButton {...props} />}
{signMethod === "qr" && <QrAccountSendButton {...props} />}
Expand Down
4 changes: 2 additions & 2 deletions apps/extension/src/ui/hooks/sapi/useScaleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { decodeMetadata } from "@talismn/scale"
import { useQuery } from "@tanstack/react-query"
import { ChainId } from "extension-core"
import { useMemo } from "react"
import { Hex } from "viem"

import { api } from "@ui/api"
import { getScaleApi, ScaleApi } from "@ui/util/scaleApi"
Expand Down Expand Up @@ -40,7 +39,8 @@ export const useScaleApi = (
metadata.metadata,
token,
chain.hasCheckMetadataHash,
hexMetadata as Hex
chain.signedExtensions,
chain.registryTypes
) as ScaleApi
},
refetchInterval: false,
Expand Down
4 changes: 3 additions & 1 deletion apps/extension/src/ui/util/getExtrinsicDispatchInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GenericExtrinsic } from "@polkadot/types"
import { assert } from "@polkadot/util"
import { HexString } from "@polkadot/util/types"
import { log } from "extension-shared"

import { stateCall } from "./stateCall"

Expand All @@ -15,6 +16,7 @@ export const getExtrinsicDispatchInfo = async (
blockHash?: HexString
): Promise<ExtrinsicDispatchInfo> => {
assert(signedExtrinsic.isSigned, "Extrinsic must be signed (or fakeSigned) in order to query fee")
const stop = log.timer("[sapi] getExtrinsicDispatchInfo")

const len = signedExtrinsic.registry.createType("u32", signedExtrinsic.encodedLength)

Expand All @@ -26,7 +28,7 @@ export const getExtrinsicDispatchInfo = async (
blockHash,
true
)

stop()
return {
partialFee: dispatchInfo.partialFee.toString(),
}
Expand Down
138 changes: 105 additions & 33 deletions apps/extension/src/ui/util/scaleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {
enhanceEncoder,
metadata as metadataCodec,
u16,
V14,
V15,
} from "@polkadot-api/substrate-bindings"
import { mergeUint8, toHex } from "@polkadot-api/utils"
import { Metadata, TypeRegistry } from "@polkadot/types"
import { ExtDef } from "@polkadot/types/extrinsic/signedExtensions/types"
import { IRuntimeVersionBase } from "@polkadot/types/types"
import { getDynamicBuilder, getLookupFn, V14, V15 } from "@talismn/scale"
import { sleep } from "@talismn/util"
import { getDynamicBuilder, getLookupFn } from "@talismn/scale"
import { ChainId, SignerPayloadJSON } from "extension-core"
import { DEBUG, log } from "extension-shared"
import { Binary } from "polkadot-api"
Expand All @@ -23,12 +25,18 @@ type ScaleMetadata = V14 | V15
type ScaleBuilder = ReturnType<typeof getDynamicBuilder>
export type ScaleApi = ReturnType<typeof getScaleApi>

export type PayloadSignerConfig = {
address: string
tip?: bigint
}

export const getScaleApi = (
chainId: ChainId,
metadata: ScaleMetadata,
token: { symbol: string; decimals: number },
hasCheckMetadataHash?: boolean,
hexMetadata?: Hex
signedExtensions?: ExtDef,
registryTypes?: unknown
) => {
const lookup = getLookupFn(metadata)
const builder = getDynamicBuilder(lookup)
Expand All @@ -52,7 +60,8 @@ export const getScaleApi = (
specVersion,
transactionVersion,
base58Prefix,
hexMetadata,
signedExtensions,
registryTypes,
}

return {
Expand All @@ -77,36 +86,64 @@ export const getScaleApi = (
config: PayloadSignerConfig
) => getSignerPayloadJSON(chainId, metadata, builder, pallet, method, args, config, chainInfo),

getFeeEstimate: async (payload: SignerPayloadJSON) =>
getFeeEstimate: (payload: SignerPayloadJSON) =>
getFeeEstimate(chainId, metadata, builder, payload, chainInfo),

getTypeRegistry: (payload: SignerPayloadJSON) => getTypeRegistry(metadata, payload, chainInfo),

submit: (payload: SignerPayloadJSON, signature?: Hex) => api.subSubmit(payload, signature),
}
}

export type PayloadSignerConfig = {
address: string
tip?: bigint
}

type ChainInfo = {
specName: string
specVersion: number
transactionVersion: number
base58Prefix: number
token: { symbol: string; decimals: number }
hasCheckMetadataHash?: boolean
hexMetadata?: Hex // TODO REMOVE
signedExtensions?: ExtDef
registryTypes?: unknown
}

const getTypeRegistry = (
metadata: ScaleMetadata,
payload: SignerPayloadJSON,
chainInfo: ChainInfo
) => {
const stop = log.timer("[sapi] getTypeRegistry")
const fullMetadata = {
magicNumber: 1635018093, // magic number for metadata
metadata: { tag: "v15" as const, value: metadata as V15 },
}
const metadataBytes = metadataCodec.enc(fullMetadata) // ~30ms

const registry = new TypeRegistry()

// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (chainInfo.registryTypes) registry.register(chainInfo.registryTypes as any)

registry.setMetadata(
new Metadata(registry, metadataBytes),
payload.signedExtensions,
chainInfo.signedExtensions
) // ~30ms

stop()
return registry
}

const getPayloadWithMetadataHash = (
metadata: ScaleMetadata,
builder: ScaleBuilder,
chainInfo: ChainInfo,
payload: SignerPayloadJSON
) => {
): { payload: SignerPayloadJSON; txMetadata?: Uint8Array } => {
if (!chainInfo.hasCheckMetadataHash)
return { payload: { ...payload, mode: 0, metadataHash: undefined }, txMetadata: undefined }
return {
payload: { ...payload, mode: 0, metadataHash: undefined, withSignedTransaction: true },
txMetadata: undefined,
}

try {
// merkleizeMetadata method expects versioned metadata so we need to reencode our V15 object back to a versioned one (~30ms)
Expand Down Expand Up @@ -136,25 +173,34 @@ const getPayloadWithMetadataHash = (

const metadataHash = toHex(merkleizedMetadata.digest()) as Hex

const payloadWithMetadataHash = {
...payload,
mode: 1,
metadataHash,
withSignedTransaction: true,
}

// TODO do this without PJS / registry
// const { extra, additionalSigned } = getSignedExtensionValues(payload, metadata)
// const badExtPayload = mergeUint8(fromHex(payload.method), ...extra, ...additionalSigned)
// log.debug("[sapi] bad ExtPayload", { badExtPayload })

const stop2 = log.timer("get ExtrinsicPayload using PJS")
const registry = new TypeRegistry()
registry.setMetadata(new Metadata(registry, metadataBytes), payload.signedExtensions)
const extPayload = registry.createType("ExtrinsicPayload", payload)
const registry = getTypeRegistry(metadata, payload, chainInfo)
const extPayload = registry.createType("ExtrinsicPayload", payloadWithMetadataHash)
const barePayload = extPayload.toU8a(true)
stop2()
log.debug("[sapi] good ExtPayload", { barePayload })

const txMetadata = merkleizedMetadata.getProofForExtrinsicPayload(barePayload)

return { payload: { ...payload, mode: 1, metadataHash }, txMetadata }
return {
payload: payloadWithMetadataHash,
txMetadata,
}
} catch (err) {
log.error("Failed to get shortened metadata", { error: err })
return { payload: { ...payload, mode: 0, metadataHash: undefined }, txMetadata: undefined }
return {
payload: { ...payload, mode: 0, metadataHash: undefined, withSignedTransaction: true },
txMetadata: undefined,
}
}
}

Expand Down Expand Up @@ -231,37 +277,63 @@ const getFeeEstimate = async (
payload: SignerPayloadJSON,
chainInfo: ChainInfo
) => {
const fullMetadata = {
magicNumber: 1635018093, // magic number for metadata
metadata: { tag: "v15" as const, value: metadata as V15 },
}
const metadataBytes = metadataCodec.enc(fullMetadata)

const stop = log.timer("[sapi] getFeeEstimate => create Extrinsic")
const registry = new TypeRegistry()
registry.setMetadata(new Metadata(registry, metadataBytes), payload.signedExtensions)
const registry = getTypeRegistry(metadata, payload, chainInfo)
const extrinsic = registry.createType("Extrinsic", payload)
stop()

extrinsic.signFake(payload.address, {
nonce: payload.nonce,
blockHash: payload.blockHash,
genesisHash: payload.genesisHash,
//payload,
runtimeVersion: {
specVersion: chainInfo.specVersion,
transactionVersion: chainInfo.transactionVersion,
// other fields aren't necessary for signing
} as IRuntimeVersionBase,
})

await sleep(2000)
const bytes = extrinsic.toU8a(true)
const binary = Binary.fromBytes(bytes)

try {
const { partialFee } = await getRuntimeCallValue<{ partialFee: bigint }>(
chainId,
builder,
"TransactionPaymentApi",
"query_info",
[binary, bytes.length]
)
return partialFee
} catch (err) {
log.error("Failed to get fee estimate using getRuntimeCallValue", { error: err })
}

// fallback to pjs encoded state call, in case the above fails (extracting runtime calls codecs might require metadata V15)
const { partialFee } = await getExtrinsicDispatchInfo(chainId, extrinsic)

return BigInt(partialFee)
}

const getRuntimeCallValue = async <T>(
chainId: ChainId,
scaleBuilder: ScaleBuilder,
apiName: string,
method: string,
args: unknown[]
) => {
const stop = log.timer("[sapi] getRuntimeCallValue")
const call = scaleBuilder.buildRuntimeCall(apiName, method)

const hex = await api.subSend<string>(chainId, "state_call", [
`${apiName}_${method}`,
toHex(call.args.enc(args)),
])

const res = call.value.dec(hex) as T
stop()

return res
}

const getConstantValue = <T>(
metadata: ScaleMetadata,
scaleBuilder: ScaleBuilder,
Expand Down

0 comments on commit dde349f

Please sign in to comment.