Skip to content

Commit 7b1a846

Browse files
committed
chore: added a wharfkit sign request command
1 parent d79217c commit 7b1a846

8 files changed

Lines changed: 527 additions & 23 deletions

File tree

src/commands/action/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Command} from 'commander'
2+
import {createActionRequest} from './request'
3+
4+
/**
5+
* Create the sign command with subcommands
6+
*/
7+
export function createSignCommand(): Command {
8+
const signCommand = new Command('sign')
9+
signCommand.description('Create signing requests (ESR) for blockchain actions')
10+
11+
// sign request - Create a signing request (ESR) and display QR code
12+
signCommand
13+
.command('request')
14+
.description('Create a signing request (ESR) and display QR code for any action')
15+
.argument('<contract::action>', 'Contract and action in format "contract::action"')
16+
.argument('<data>', 'Action data as JSON or key=value pairs')
17+
.option(
18+
'-c, --chain <chain>',
19+
'Chain name or API URL (e.g., local, Jungle4, EOS, https://...)',
20+
'local'
21+
)
22+
.option(
23+
'-a, --auth <authorization>',
24+
'Authorization in format "account@permission" (default: wallet placeholder)'
25+
)
26+
.action(async (contractAction, data, options) => {
27+
await createActionRequest(contractAction, data, options)
28+
})
29+
30+
return signCommand
31+
}

src/commands/action/request.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/* eslint-disable no-console */
2+
import {ABI, APIClient, FetchProvider} from '@wharfkit/antelope'
3+
import {Chains} from '@wharfkit/common'
4+
import {PlaceholderName, PlaceholderPermission, SigningRequest} from '@wharfkit/signing-request'
5+
import fetch from 'node-fetch'
6+
import {displayQRCode} from '../../utils'
7+
8+
export interface ActionRequestOptions {
9+
chain?: string
10+
auth?: string
11+
}
12+
13+
/**
14+
* Parse authorization string (e.g., "account@permission" or "account")
15+
* Returns undefined for actor/permission to use placeholders
16+
*/
17+
export function parseAuthorization(authString?: string): {
18+
actor: string | null
19+
permission: string
20+
} {
21+
if (!authString) {
22+
return {actor: null, permission: 'active'}
23+
}
24+
25+
if (authString.includes('@')) {
26+
const [actor, permission] = authString.split('@')
27+
return {actor, permission: permission || 'active'}
28+
}
29+
30+
return {actor: authString, permission: 'active'}
31+
}
32+
33+
/**
34+
* Parse action data from string (JSON or key=value pairs)
35+
*/
36+
export function parseActionData(dataString: string): Record<string, unknown> {
37+
// Try JSON first
38+
try {
39+
return JSON.parse(dataString)
40+
} catch {
41+
// Try key=value format
42+
const data: Record<string, unknown> = {}
43+
const pairs = dataString.split(',').map((p) => p.trim())
44+
45+
for (const pair of pairs) {
46+
const [key, ...valueParts] = pair.split('=')
47+
if (key && valueParts.length > 0) {
48+
const value = valueParts.join('=').trim()
49+
// Try to parse as number or boolean
50+
if (value === 'true') {
51+
data[key.trim()] = true
52+
} else if (value === 'false') {
53+
data[key.trim()] = false
54+
} else if (!isNaN(Number(value)) && value !== '') {
55+
data[key.trim()] = Number(value)
56+
} else {
57+
data[key.trim()] = value
58+
}
59+
}
60+
}
61+
62+
if (Object.keys(data).length === 0) {
63+
throw new Error(
64+
`Invalid action data format. Use JSON (e.g., '{"key": "value"}') or key=value pairs (e.g., 'key1=value1,key2=value2')`
65+
)
66+
}
67+
68+
return data
69+
}
70+
}
71+
72+
/**
73+
* Parse contract::action format and validate
74+
*/
75+
export function parseContractAction(contractAction: string): {
76+
contractAccount: string
77+
actionName: string
78+
} {
79+
if (!contractAction.includes('::')) {
80+
throw new Error(
81+
`Invalid format. Use contract::action format (e.g., "eosio.token::transfer")`
82+
)
83+
}
84+
85+
const [contractAccount, actionName] = contractAction.split('::')
86+
87+
if (!contractAccount || !actionName) {
88+
throw new Error(
89+
`Invalid format. Use contract::action format (e.g., "eosio.token::transfer")`
90+
)
91+
}
92+
93+
return {contractAccount, actionName}
94+
}
95+
96+
/**
97+
* Get the API URL for a chain name or URL
98+
*/
99+
export function getApiUrl(chainOrUrl: string): string {
100+
// Check if it's already a URL
101+
if (chainOrUrl.startsWith('http://') || chainOrUrl.startsWith('https://')) {
102+
return chainOrUrl
103+
}
104+
105+
// Check if it matches a known chain key from @wharfkit/common
106+
const knownChainKey = Object.keys(Chains).find(
107+
(key) => key.toLowerCase() === chainOrUrl.toLowerCase()
108+
)
109+
110+
if (knownChainKey) {
111+
return (Chains as Record<string, {url: string}>)[knownChainKey].url
112+
}
113+
114+
// Default to local
115+
if (chainOrUrl === 'local') {
116+
return 'http://127.0.0.1:8888'
117+
}
118+
119+
throw new Error(
120+
`Unknown chain: ${chainOrUrl}. Use a full URL (http://...) or a known chain name (EOS, Jungle4, WAX, etc.)`
121+
)
122+
}
123+
124+
/**
125+
* Create an ESR (EOSIO Signing Request) for any action
126+
*/
127+
export async function createActionESR(
128+
client: APIClient,
129+
contractAccount: string,
130+
actionName: string,
131+
actionData: Record<string, unknown>,
132+
auth: {actor: string | null; permission: string}
133+
): Promise<{uri: string; encodedUri: string}> {
134+
const info = await client.v1.chain.get_info()
135+
const chainId = String(info.chain_id)
136+
137+
// Fetch the contract ABI for serialization
138+
const abiResponse = await client.v1.chain.get_abi(contractAccount)
139+
if (!abiResponse.abi) {
140+
throw new Error(`Could not fetch ABI for contract: ${contractAccount}`)
141+
}
142+
const contractAbi = ABI.from(abiResponse.abi)
143+
144+
// Verify the action exists in the ABI
145+
const actionDef = contractAbi.actions.find((a) => String(a.name) === actionName)
146+
if (!actionDef) {
147+
const availableActions = contractAbi.actions.map((a) => String(a.name)).join(', ')
148+
throw new Error(
149+
`Action "${actionName}" not found in contract "${contractAccount}". Available actions: ${availableActions}`
150+
)
151+
}
152+
153+
// Use placeholders if no specific actor is provided
154+
const actor = auth.actor || PlaceholderName
155+
const permission = auth.actor ? auth.permission : PlaceholderPermission
156+
157+
// Replace placeholder tokens in data with actual placeholders
158+
const processedData = processDataPlaceholders(actionData, auth.actor)
159+
160+
// Create the signing request
161+
const request = await SigningRequest.create(
162+
{
163+
action: {
164+
account: contractAccount,
165+
name: actionName,
166+
authorization: [
167+
{
168+
actor,
169+
permission,
170+
},
171+
],
172+
data: processedData,
173+
},
174+
chainId,
175+
},
176+
{
177+
abiProvider: {
178+
getAbi: async () => contractAbi,
179+
},
180+
}
181+
)
182+
183+
const encodedUri = request.encode()
184+
// Normalize to esr:// format
185+
let uri = encodedUri
186+
if (!uri.startsWith('esr://')) {
187+
if (uri.startsWith('esr:')) {
188+
uri = `esr://${uri.slice(4)}`
189+
}
190+
}
191+
192+
return {uri, encodedUri}
193+
}
194+
195+
/**
196+
* Process data to replace special placeholder tokens
197+
* Replaces $signer with the PlaceholderName for ESR
198+
*/
199+
function processDataPlaceholders(
200+
data: Record<string, unknown>,
201+
specificActor: string | null
202+
): Record<string, unknown> {
203+
const processed: Record<string, unknown> = {}
204+
205+
for (const [key, value] of Object.entries(data)) {
206+
if (typeof value === 'string' && value === '$signer') {
207+
processed[key] = specificActor || PlaceholderName
208+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
209+
processed[key] = processDataPlaceholders(
210+
value as Record<string, unknown>,
211+
specificActor
212+
)
213+
} else {
214+
processed[key] = value
215+
}
216+
}
217+
218+
return processed
219+
}
220+
221+
/**
222+
* Create a signing request and display QR code
223+
*/
224+
export async function createActionRequest(
225+
contractAction: string,
226+
dataString: string,
227+
options: ActionRequestOptions
228+
): Promise<void> {
229+
// Parse contract::action format
230+
const {contractAccount, actionName} = parseContractAction(contractAction)
231+
232+
// Parse action data
233+
const actionData = parseActionData(dataString)
234+
235+
// Parse authorization
236+
const auth = parseAuthorization(options.auth)
237+
238+
// Get the API URL
239+
const url = getApiUrl(options.chain || 'local')
240+
241+
console.log('Creating signing request...')
242+
console.log(` Contract: ${contractAccount}`)
243+
console.log(` Action: ${actionName}`)
244+
console.log(` Chain: ${options.chain || 'local'} (${url})`)
245+
console.log(` Data: ${JSON.stringify(actionData, null, 2)}`)
246+
247+
if (auth.actor) {
248+
console.log(` Authorization: ${auth.actor}@${auth.permission}`)
249+
} else {
250+
console.log(` Authorization: <wallet signer>@<wallet permission>`)
251+
}
252+
253+
try {
254+
const client = new APIClient({
255+
provider: new FetchProvider(url, {fetch}),
256+
})
257+
258+
const {uri} = await createActionESR(client, contractAccount, actionName, actionData, auth)
259+
260+
displayQRCode(uri, `📝 ${contractAccount}::${actionName}`)
261+
262+
console.log(`\n✅ Signing request created!`)
263+
console.log(` Scan the QR code with a compatible wallet to sign and broadcast.`)
264+
} catch (error) {
265+
console.error(`\n❌ Failed to create signing request: ${(error as Error).message}`)
266+
process.exit(1)
267+
}
268+
}

src/commands/contract/deploy-utils.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as readline from 'readline'
33
import type {APIClient} from '@wharfkit/antelope'
44
import {ABI, Asset, Struct} from '@wharfkit/antelope'
55
import {PlaceholderName, PlaceholderPermission, SigningRequest} from '@wharfkit/signing-request'
6-
import * as qrcode from 'qrcode-terminal'
6+
import {displayQRCode} from '../../utils'
77

88
/**
99
* RAM market row structure
@@ -67,8 +67,8 @@ export function calculateRamNeeded(wasmSize: number, abiSize: number): number {
6767
const setcodeRam = wasmSize * 10
6868
// setabi action requires roughly the ABI file size
6969
const setabiRam = abiSize
70-
// Add a 10% buffer for overhead
71-
const buffer = Math.ceil((setcodeRam + setabiRam) * 0.1)
70+
// Add a 1% buffer for overhead
71+
const buffer = Math.ceil((setcodeRam + setabiRam) * 0.01)
7272
return setcodeRam + setabiRam + buffer
7373
}
7474

@@ -443,18 +443,6 @@ export async function createBuyRamESR(
443443
return {uri, encodedUri}
444444
}
445445

446-
/**
447-
* Display QR code and link in terminal
448-
*/
449-
export function displayQRCode(uri: string, title: string): void {
450-
console.log(`\n${title}`)
451-
console.log('─'.repeat(60))
452-
console.log(`\nLink: ${uri}`)
453-
console.log('\nScan this QR code with your wallet app:\n')
454-
qrcode.generate(uri, {small: true})
455-
console.log('─'.repeat(60))
456-
}
457-
458446
/**
459447
* Wait for account balance to reach a target
460448
*/

src/commands/contract/deploy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import {getKeyFromWallet, listWalletKeys} from '../wallet/utils'
1111

1212
import {Chains} from '@wharfkit/common'
1313
import {compileContract} from '../compile'
14+
import {displayQRCode} from '../../utils'
1415
import {
1516
analyzeRamRequirements,
1617
createTransferESR,
17-
displayQRCode,
1818
displayRamAnalysis,
1919
formatBytes,
2020
promptConfirmation,

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {createDevCommand} from './commands/dev'
99
import {createWalletCommand} from './commands/wallet/index'
1010
import {createAccountCommand} from './commands/account'
1111
import {createTableCommand} from './commands/table'
12+
import {createSignCommand} from './commands/action'
1213

1314
const program = new Command()
1415

@@ -56,4 +57,7 @@ program.addCommand(createAccountCommand())
5657
// 9. Command to lookup table data (uses default chain)
5758
program.addCommand(createTableCommand())
5859

60+
// 10. Command to create signing requests
61+
program.addCommand(createSignCommand())
62+
5963
program.parse(process.argv)

src/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
/* eslint-disable no-console */
12
import {APIClient, FetchProvider} from '@wharfkit/antelope'
23
import {capitalize} from '@wharfkit/contract'
34
import fetch from 'node-fetch'
5+
import * as qrcode from 'qrcode-terminal'
46

57
type logLevel = 'info' | 'debug'
68

@@ -25,3 +27,15 @@ export function capitalizeName(text: string) {
2527
export function formatClassName(name: string) {
2628
return name.split(/[.]/).join('')
2729
}
30+
31+
/**
32+
* Display QR code and link in terminal
33+
*/
34+
export function displayQRCode(uri: string, title: string): void {
35+
console.log(`\n${title}`)
36+
console.log('─'.repeat(60))
37+
console.log(`\nLink: ${uri}`)
38+
console.log('\nScan this QR code with your wallet app:\n')
39+
qrcode.generate(uri, {small: true})
40+
console.log('─'.repeat(60))
41+
}

0 commit comments

Comments
 (0)