Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new cli commands accounts:notes and accounts:transactions #1086

Merged
merged 15 commits into from Jun 15, 2022
2 changes: 2 additions & 0 deletions ironfish-cli/README.md
Expand Up @@ -32,6 +32,8 @@ Interact with the node in a new terminal window:
- Request a small amount of $IRON for testing payments
- `yarn start accounts:pay`
- Send $IRON to another account
- `yarn start accounts:transactions [account]`
- Display transactions from and to your account

### Start a node and start mining
Run these commands in two different terminals:
Expand Down
63 changes: 63 additions & 0 deletions ironfish-cli/src/commands/accounts/transactions.test.ts
@@ -0,0 +1,63 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { expect as expectCli, test } from '@oclif/test'
import { GetTransactionsResponse } from 'ironfish'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import didn't work for me, I had to change the path to ../../../../ironfish/src/rpc/routes/transactions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, this needs updating for the latest version. I just pushed an update.


describe('accounts:transactions', () => {
const responseContent: GetTransactionsResponse = {
account: 'default',
notes: [
{
isSpender: true,
txHash: '1fa5f38c446e52f8842d8c861507744fc3f354992610e1661e033ef316e2d3d1',
txFee: '1',
isMinerFee: false,
amount: '1',
memo: 'foo',
},
],
}

beforeAll(() => {
jest.doMock('ironfish', () => {
const originalModule = jest.requireActual('ironfish')
const client = {
connect: jest.fn(),
getTransactionNotes: jest.fn().mockImplementation(() => ({
content: responseContent,
})),
}
const module: typeof jest = {
...originalModule,
IronfishSdk: {
init: jest.fn().mockImplementation(() => ({
connectRpc: jest.fn().mockResolvedValue(client),
client,
})),
},
}
return module
})
})

afterAll(() => {
jest.dontMock('ironfish')
})

describe('fetching the transaction notes for an account', () => {
test
.stdout()
.command(['accounts:transactions', 'default'])
.exit(0)
.it('logs the notes for the given account', (ctx) => {
expectCli(ctx.stdout).include(responseContent.account)
expectCli(ctx.stdout).include(responseContent.notes[0].isSpender ? `✔` : `x`)
expectCli(ctx.stdout).include(responseContent.notes[0].txHash)
expectCli(ctx.stdout).include(responseContent.notes[0].txFee)
expectCli(ctx.stdout).include(responseContent.notes[0].isMinerFee ? `✔` : `x`)
expectCli(ctx.stdout).include(responseContent.notes[0].amount)
expectCli(ctx.stdout).include(responseContent.notes[0].memo)
})
})
})
63 changes: 63 additions & 0 deletions ironfish-cli/src/commands/accounts/transactions.ts
@@ -0,0 +1,63 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { CliUx } from '@oclif/core'
import { IronfishCommand } from '../../command'
import { RemoteFlags } from '../../flags'

export class TransactionsCommand extends IronfishCommand {
static description = `Display the account transactions`

static flags = {
...RemoteFlags,
}

static args = [
{
name: 'account',
parse: (input: string): Promise<string> => Promise.resolve(input.trim()),
required: false,
description: 'name of the account to get transactions for',
},
]

async start(): Promise<void> {
const { args } = await this.parse(TransactionsCommand)
const account = args.account as string | undefined

const client = await this.sdk.connectRpc()

const response = await client.getTransactionNotes({
account,
})

const { account: accountResponse, notes } = response.content

this.log(`\n ${String(accountResponse)} - Transaction notes\n`)

CliUx.ux.table(notes, {
isSpender: {
header: 'Spender',
get: (row) => (row.isSpender ? `✔` : `x`),
},
isMinerFee: {
header: 'Miner Fee',
get: (row) => (row.isMinerFee ? `✔` : `x`),
},
amount: {
header: 'Amount ($ORE)',
},
txFee: {
header: 'Tx Fee ($ORE)',
},
txHash: {
header: 'Tx Hash',
},
memo: {
header: 'Memo',
},
})

this.log(`\n`)
}
}
46 changes: 46 additions & 0 deletions ironfish/src/account/accounts.ts
Expand Up @@ -596,6 +596,52 @@ export class Accounts {
this.scan = null
}

async getTransactionNotes(account: Account): Promise<{
notes: {
isSpender: boolean
txHash: string
txFee: string
isMinerFee: boolean
amount: string
memo: string
}[]
}> {
this.assertHasAccount(account)

const notes = []

for (const transactionMapValue of this.transactionMap.values()) {
const transaction = transactionMapValue.transaction

for (const note of transaction.notes()) {
// Try decrypting the note as the owner
let decryptedNote = note.decryptNoteForOwner(account.incomingViewKey)
let isSpender = false

if (!decryptedNote) {
// Try decrypting the note as the spender
decryptedNote = note.decryptNoteForSpender(account.outgoingViewKey)
isSpender = true
}

if (decryptedNote && decryptedNote.value() !== BigInt(0)) {
notes.push({
isSpender,
txHash: transaction.hash().toString('hex'),
txFee: String(await transaction.fee()),
isMinerFee: await transaction.isMinersFee(),
amount: String(decryptedNote.value()),
memo: decryptedNote.memo().replace(/\x00/g, ''),
})

continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this continue needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, removed.

}
}
}

return { notes }
}

private async getUnspentNotes(
account: Account,
): Promise<ReadonlyArray<{ hash: string; note: Note; index: number | null }>> {
Expand Down
11 changes: 11 additions & 0 deletions ironfish/src/rpc/clients/rpcClient.ts
Expand Up @@ -30,6 +30,8 @@ import {
GetPublicKeyResponse,
GetStatusRequest,
GetStatusResponse,
GetTransactionsRequest,
GetTransactionsResponse,
GetWorkersStatusRequest,
GetWorkersStatusResponse,
NewBlocksStreamRequest,
Expand Down Expand Up @@ -259,6 +261,15 @@ export abstract class IronfishRpcClient {
return this.request<void, OnGossipResponse>(`${ApiNamespace.event}/onGossip`, params)
}

async getTransactionNotes(
params: GetTransactionsRequest = {},
): Promise<ResponseEnded<GetTransactionsResponse>> {
return this.request<GetTransactionsResponse>(
`${ApiNamespace.transaction}/getTransactionNotes`,
params,
).waitForEnd()
}

async sendTransaction(
params: SendTransactionRequest,
): Promise<ResponseEnded<SendTransactionResponse>> {
Expand Down
56 changes: 56 additions & 0 deletions ironfish/src/rpc/routes/transactions/getTransactionNotes.ts
@@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import * as yup from 'yup'
import { getAccount } from '../accounts/utils'
import { ApiNamespace, router } from '../router'

export type GetTransactionsRequest = { account?: string }

export type GetTransactionsResponse = {
account: string
notes: {
isSpender: boolean
txHash: string
txFee: string
isMinerFee: boolean
amount: string
memo: string
}[]
}

export const GetTransactionsRequestSchema: yup.ObjectSchema<GetTransactionsRequest> = yup
.object({
account: yup.string().strip(true),
})
.defined()

export const GetTransactionsResponseSchema: yup.ObjectSchema<GetTransactionsResponse> = yup
.object({
account: yup.string().defined(),
notes: yup
.array(
yup
.object({
isSpender: yup.boolean().defined(),
txHash: yup.string().defined(),
txFee: yup.string().defined(),
isMinerFee: yup.boolean().defined(),
amount: yup.string().defined(),
memo: yup.string().trim().defined(),
})
.defined(),
)
.defined(),
})
.defined()

router.register<typeof GetTransactionsRequestSchema, GetTransactionsResponse>(
`${ApiNamespace.transaction}/getTransactionNotes`,
GetTransactionsRequestSchema,
async (request, node): Promise<void> => {
const account = getAccount(node, request.data.account)
const { notes } = await node.accounts.getTransactionNotes(account)
request.end({ account: account.displayName, notes })
},
)
1 change: 1 addition & 0 deletions ironfish/src/rpc/routes/transactions/index.ts
Expand Up @@ -2,4 +2,5 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

export * from './getTransactionNotes'
export * from './sendTransaction'