Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
"test:watch": "vitest"
},
"dependencies": {
"polkadot-api": "^1.9.13"
"polkadot-api": "^1.9.13",
"@polkadot-labs/hdkd": "^0.0.13",
"@polkadot-labs/hdkd-helpers": "^0.0.13",
"@subsquid/ss58": "^2.0.2"
},
"devDependencies": {
"@babel/plugin-syntax-import-attributes": "^7.26.0",
Expand Down
108 changes: 108 additions & 0 deletions packages/common/src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { ed25519CreateDerive, sr25519CreateDerive } from "@polkadot-labs/hdkd"
import { entropyToMiniSecret, mnemonicToEntropy } from "@polkadot-labs/hdkd-helpers"
import * as ss58 from "@subsquid/ss58"
import type { PolkadotSigner } from "polkadot-api/signer"
import { getPolkadotSigner } from "polkadot-api/signer"

import { getAllSupportedChains, getChainById } from "../chains/chains"
import type { AgentConfig } from "../types"

/**
* Convert a public key (Uint8Array) to a Substrate address
* @param publicKey - The public key as Uint8Array (32 bytes)
* @param chainId - The chain ID to get the correct SS58 prefix
* @returns The SS58-encoded address string
*/
export function publicKeyToAddress(publicKey: Uint8Array, chainId: string = "polkadot"): string {
const chain = getChainById(chainId, getAllSupportedChains())
return ss58.codec(chain.prefix).encode(publicKey)
}

/**
* Derive and convert address from mini secret
*
* @param miniSecret - The mini secret as Uint8Array (32 bytes)
* @param keyType - The cryptographic key type ("Sr25519" or "Ed25519")
* @param derivationPath - The BIP44 derivation path (e.g., "//0", "//hard/soft")
* @param chainId - The target chain ID for address encoding (default: "polkadot")
* @returns The SS58-encoded address string for the specified chain
*
*/
export function deriveAndConvertAddress(
miniSecret: Uint8Array,
keyType: "Sr25519" | "Ed25519",
derivationPath: string,
chainId: string = "polkadot"
): string {
const keypair = getKeypair(miniSecret, keyType, derivationPath)
return publicKeyToAddress(keypair.publicKey, chainId)
}

/**
* Generate mini secret from agent config
* @param config - The agent configuration
* @returns The mini secret as Uint8Array
*/
export function generateMiniSecret(config: AgentConfig): Uint8Array {
if (!config.mnemonic && !config.privateKey) {
throw new Error("Missing mnemonic phrase or privateKey")
}

if (config.mnemonic && config.privateKey) {
throw new Error("Cannot provide both mnemonic phrase and privateKey")
}

if (config.mnemonic) {
const entropy = mnemonicToEntropy(config.mnemonic)
return entropyToMiniSecret(entropy)
} else if (config.privateKey) {
const privateKeyHex = config.privateKey.startsWith("0x")
? config.privateKey.slice(2)
: config.privateKey

return new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)))
} else {
throw new Error("No valid wallet source found")
}
}

/**
* Get keypair from mini secret and derivation path
* @param miniSecret - The mini secret
* @param keyType - The key type
* @param derivationPath - The derivation path
* @returns The derived keypair
*/
export function getKeypair(
miniSecret: Uint8Array,
keyType: "Sr25519" | "Ed25519",
derivationPath: string = ""
) {
const derive =
keyType === "Sr25519" ? sr25519CreateDerive(miniSecret) : ed25519CreateDerive(miniSecret)

return derive(derivationPath)
}

export function getSigner(
miniSecret: Uint8Array,
keyType: "Sr25519" | "Ed25519",
derivationPath: string = ""
): PolkadotSigner {
if (keyType === "Sr25519") {
const signer = getPolkadotSigner(
getKeypair(miniSecret, keyType, derivationPath).publicKey,
keyType,
input => getKeypair(miniSecret, keyType, derivationPath).sign(input)
)

return signer
} else {
const signer = getPolkadotSigner(
getKeypair(miniSecret, keyType, derivationPath).publicKey,
keyType,
input => getKeypair(miniSecret, keyType, derivationPath).sign(input)
)
return signer
}
}
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./crypto"
export * from "./formatBalance"
export * from "./isAssetHub"
export * from "./isParachain"
Expand Down
50 changes: 41 additions & 9 deletions packages/core/src/pallets/assets/transferNative.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
import type { UnsafeTransactionType } from "@polkadot-agent-kit/common"
import { type Api, type KnownChainId } from "@polkadot-agent-kit/common"

import type { TxResult } from "../../types/transaction"
import { dryRunCall, type DryRunResult } from "../../utils/dryRun"
/**
* Creates a transfer call for native assets
* @param api - The API instance to use for the transfer
* @param to - The recipient address
* @param amount - The amount to transfer
* @returns The transfer call
* Creates a transfer call for native assets with comprehensive dry run validation
*
* @param api - The API instance for the source chain
* @param from - The sender's address (SS58 format)
* @param to - The recipient's address (SS58 format)
* @param amount - The amount to transfer (in base units as BigInt)
* @returns Promise resolving to TxResult with success/failure information
*
* @throws Error If the dry run indicates the transaction would fail
*/
export const transferNativeCall = (

export const transferNativeCall = async (
api: Api<KnownChainId>,
from: string,
to: string,
amount: bigint
): UnsafeTransactionType => {
return api.tx.Balances.transfer_keep_alive({
): Promise<TxResult> => {
const tx = api.tx.Balances.transfer_keep_alive({
dest: {
type: "Id",
value: to
},
value: amount
})
const dryRunResult: DryRunResult = await dryRunCall(api, from, tx)

if (dryRunResult.value?.execution_result?.success) {
return {
success: true,
transaction: tx
}
} else {
const executionError = dryRunResult.value?.execution_result?.value

if (executionError?.error) {
const { error } = executionError

return {
success: false,
error: `${error.type} error: ${error.value?.type || "Unknown error"}`
}
}

return {
success: false,
error: "Unknown error"
}
}
}
94 changes: 62 additions & 32 deletions packages/core/src/pallets/xcm/nativeAsset.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getNativeAssets } from "@paraspell/assets"
import type { TDestination, TNodeDotKsmWithRelayChains, TPapiTransaction } from "@paraspell/sdk"
import type { TDestination, TNodeDotKsmWithRelayChains } from "@paraspell/sdk"
import { Builder } from "@paraspell/sdk"
import { parseUnits } from "@polkadot-agent-kit/common"

import type { XcmTransferResult } from "../../types/xcm"
/**
* Builds an XCM transaction to transfer a native asset from one chain to another.
*
Expand All @@ -14,17 +16,7 @@ import { parseUnits } from "@polkadot-agent-kit/common"
* @param from - The sender's address on the source chain
* @param to - The recipient's address on the destination chain
* @param amount - The amount of the native asset to transfer (as bigint, in base units)
* @returns A Promise resolving to a TPapiTransaction object representing the unsigned XCM transaction
*
* @example
* const tx = await xcmTransferNativeAsset(
* 'polkadot',
* 'hydra',
* 'senderAddress',
* 'recipientAddress',
* 10000000000n
* )
* // tx can then be signed and submitted using the appropriate transaction handler
* @returns Promise resolving to XcmTransferResult with detailed success/failure information
*/

export const xcmTransferNativeAsset = async (
Expand All @@ -33,31 +25,69 @@ export const xcmTransferNativeAsset = async (
from: string,
to: string,
amount: string
): Promise<TPapiTransaction> => {
const nativeSymbol = getNativeAssets(srcChain as TNodeDotKsmWithRelayChains)
const decimals = nativeSymbol[0].decimals || 10
const parsedAmount = parseUnits(amount, decimals)

// Dry run the XCM transfer native token
const dryRunTx = await Builder()
.from(srcChain as TNodeDotKsmWithRelayChains)
.senderAddress(from)
.to(destChain as TDestination)
.currency({ symbol: nativeSymbol[0].symbol, amount: parsedAmount })
.address(to)
.dryRun()
if (dryRunTx.origin?.success && dryRunTx.destination?.success) {
// XCM transfer native tokken
const tx = await Builder()
): Promise<XcmTransferResult> => {
try {
const nativeSymbol = getNativeAssets(srcChain as TNodeDotKsmWithRelayChains)

const decimals = nativeSymbol[0].decimals || 10
const parsedAmount = parseUnits(amount, decimals)

// Dry run the XCM transfer native token
const dryRunTx = await Builder()
.from(srcChain as TNodeDotKsmWithRelayChains)
.senderAddress(from)
.to(destChain as TDestination)
.currency({ symbol: nativeSymbol[0].symbol, amount: parsedAmount })
.address(to)
.build()
.dryRun()

const dryRunDetails = {
originSuccess: dryRunTx.origin?.success || false,
destinationSuccess: dryRunTx.destination?.success || false,
originError: dryRunTx.origin?.success ? undefined : dryRunTx.origin?.failureReason,
destinationError: dryRunTx.destination?.success
? undefined
: dryRunTx.destination?.failureReason
}

if (dryRunTx.origin?.success && dryRunTx.destination?.success) {
// Build the actual XCM transaction
const tx = await Builder()
.from(srcChain as TNodeDotKsmWithRelayChains)
.senderAddress(from)
.to(destChain as TDestination)
.currency({ symbol: nativeSymbol[0].symbol, amount: parsedAmount })
.address(to)
.build()

return {
success: true,
transaction: tx,
dryRunDetails
}
} else {
const errorDetails = []
if (!dryRunTx.origin?.success) {
errorDetails.push(
`Origin chain error: ${dryRunTx.origin?.failureReason || "Unknown error"}`
)
}
if (!dryRunTx.destination?.success) {
errorDetails.push(
`Destination chain error: ${dryRunTx.destination?.failureReason || "Unknown error"}`
)
}

return tx
} else {
throw Error("XCM dry run failed")
return {
success: false,
error: `XCM dry run failed: ${errorDetails.join("; ")}`,
dryRunDetails
}
}
} catch (error) {
return {
success: false,
error: `XCM transaction dry run failed: ${error instanceof Error ? error.message : String(error)}`
}
}
}
1 change: 1 addition & 0 deletions packages/core/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./identity"
export * from "./transaction"
export * from "./xcm"
6 changes: 6 additions & 0 deletions packages/core/src/types/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ export function isTxWithPolkadotSigner(
typeof options.signer.signBytes === "function"
)
}

export interface TxResult {
success: boolean
transaction?: UnsafeTransactionType
error?: string
}
13 changes: 13 additions & 0 deletions packages/core/src/types/xcm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { TPapiTransaction } from "@paraspell/sdk"

export interface XcmTransferResult {
success: boolean
transaction?: TPapiTransaction
error?: string
dryRunDetails?: {
originSuccess: boolean
destinationSuccess: boolean
originError?: string
destinationError?: string
}
}
Loading