Skip to content

Commit

Permalink
adds custom transaction funding by note hash (#3830)
Browse files Browse the repository at this point in the history
* adds custom transaction funding by note hash

supporting custom transaction funding will allow users and developers to choose
the notes to spend in a transaction using whatever note selection algorithm they
choose.

adds optional 'spendNoteHashes' parameter to 'wallet/createTransaction' rpc
endpoint to accept a list of hex string note hashes to use to fund a
transaction.

implements 'fundWithNoteHashes' to load decrypted notes from the specified
hashes and use them to build the spends for a raw transaction. ensures that the
specified notes have enough value to fund the transaction. requires that notes
have an index so that a witness can be created.

does not check whether the notes have already been spent. this is intentional to
support use cases like 'canceling' or 'speeding up' a transaction by creating a
transaction that spends the same notes but offers a higher fee.

adds unit tests for wallet and rpc transaction creation methods.

* adds error messages to assertions in 'fundFromNoteHashes'

* supports partial funding from notes passed to createTransaction

if the notes passed to createTransaction do not cover the amount needed, the
wallet will fund the rest of the transaction using available unspent notes.

refactors fund to optionally take a list of notes.

renames 'createSpendsForAsset' to 'addSpendsForAsset' and modifies to take a set
of notes already spent and an amount already spent.

removes unneeded code: 'checkNoteSpentOnChainAndRepair', 'createSpends'

renames 'spendNoteHashes' to 'notes' in RPC and createTransaction method

* iterate over amounts needed map entries instead of keys

avoid defaulting to 0 if the key is missing.

* defaults notes to [] in fund, unindents
  • Loading branch information
hughy authored Apr 25, 2023
1 parent 1c2c600 commit bb99849
Show file tree
Hide file tree
Showing 6 changed files with 677 additions and 343 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -687,5 +687,96 @@
}
]
}
],
"Route wallet/createTransaction should generate a valid transaction by spending the specified notes": [
{
"version": 2,
"id": "b09f2e3e-ba86-480e-a503-ce78f1870d32",
"name": "existingAccount",
"spendingKey": "bafc123b3ee9e5a3ff7ad41c20ea0ec778907f9c1fe9287ac799df43a8b296c7",
"viewKey": "cf9b3d304a8febca46713c4628312eb422e44256492b7111bfcad5bbd75e3914729d897e9ca50114b1f771a2420e7379005980e466b4763ab7cb836ec115f646",
"incomingViewKey": "80601f98f2a89b6ee1a75ac52742bea16eedf75d4e3ea7e9d07d853bd3857303",
"outgoingViewKey": "ddf3487844b91e284abf2d324c7bc7a555bd682fa801efd3c1cafb0cf71974c2",
"publicAddress": "81159b88c6958a5b1e294bcc51bb733ff3c126298f909682921ebe22b094586d",
"createdAt": null
},
{
"header": {
"sequence": 2,
"previousBlockHash": "88B6FA8D745A4E53BDA001318E60B04EE2E4EE06A38095688D58049CB6F15ACA",
"noteCommitment": {
"type": "Buffer",
"data": "base64:v0jKOhi7/kWoAzfjAuvb267KKqnOUDOvSpwM6jxZ0WI="
},
"transactionCommitment": {
"type": "Buffer",
"data": "base64:t9yKkVISPQPjyG3khNcZX5jWMPx1bzgKTy5o5Po40fU="
},
"target": "883423532389192164791648750371459257913741948437809479060803100646309888",
"randomness": "0",
"timestamp": 1682349763914,
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
"noteSize": 4,
"work": "0"
},
"transactions": [
{
"type": "Buffer",
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAA6SYz5mJZGNRglZJN/SxR222hT2XAPr//VByBfr5hqIyuYT9Szj8X5FvjMBZdLifzKIyqlh93NqW/2bRa4AgeWVy8QQANGUM1ghCItNSI2rOYE8tPVefJP1e2BBcjhMLUUi2sHynG8008vSoNqMQu+hPE5Ga59hidpN38My+LCWYZBS7yrqBKt6yLBP/vYDCyhML1xTSw9N5eEOVfJ0+wXt4V6Ru3LXOUT15nFxyeCu2KDpFV+iHyOidX9E7CoQ75z5sjxqfBT7dr4sIvbXVHEhbZjq4B1kcXVqnIb5cv1q2ykbeWPDZGO5cgIU7BxZYSvYARbOpYGmTyecyg/A5/iH5nq2XvlFYhdhbLAi6WcHnkPbY5JrEm9rHoVUcFbUpxqc8rXBssiAOAoAeOaGbB3VjNc1lhBvKjjGn9o/+PTTw3Th1vkkH4dvkWEDwYJ8VgM8Oc6KupGkSVev5/yayDNw2gQ6Rr06MInXsuoYiH7HxlOxbvKnu2eLgW0kfuNqM2QGb20vAJD5KHTsLhCChJF0uGaT0FLmw3JL5MvS35JDwQqa/zFfTGLxw0RLO8gHk9Y+aIigrRliKpWzU37nHuoBXFXoeZz3SWdRGI/VmJ9DlUIDbGVv0TEUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwqkYvxploPJeAdEVjpSEmGdfTW6bmemOKKq2PAEErlOJHmy1PCKYYmYe8pOpxQNolMujZMhkEQC0auebFHUzEDA=="
}
]
},
{
"header": {
"sequence": 3,
"previousBlockHash": "C4F9242D70C37D1E6063D3B88A4A49ED10C259308159D1F3A5DB7BFFCA27E2CD",
"noteCommitment": {
"type": "Buffer",
"data": "base64:k+sgzHSx9NS9/2YWeH+GvnOnoOf/44S6ijkqFTpkgzo="
},
"transactionCommitment": {
"type": "Buffer",
"data": "base64:J9nwgBrYHDb5/UMm2CvopP0q5bnqCMOEz5i+fvVpJS4="
},
"target": "880842937844725196442695540779332307793253899902937591585455087694081134",
"randomness": "0",
"timestamp": 1682349764614,
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
"noteSize": 5,
"work": "0"
},
"transactions": [
{
"type": "Buffer",
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAAj24sldcyTytI7FloX/QZMef0t4NIqBUvota3BYf6ee20I/rbq9GlGr4iXaV9mF0WFHaobvnYC9kqkOZgxOCa7DrYQ71GEOBb7/s01ZnfauOZMV4UsWa3pZIMq+7ob7/sXA1hOCZe4s5HyCphrpAJ9ndSEcptYdd/rcAoHi3FgQ4XdRl/ob5+sY2w5vZh4n/dhLVWe6euvd45jmBXHgehh+zJmQNJ6Qlc3nDNzkILygaAmQi4U2PvP5Hgdh24Xsyi1jtTt8F3BmNzFG9fdo+aNCg6lIAkMkAGwJ0UWKCnbmaIurE7qxpO9gYiMEWgzjkcvau8DiTftqXUTEZQG1wXm/eX4eeqkvk5PP+jmVoV47SVwcxkrgTzbRitQOSGLt5ZRkChTXGylihlrezmercVksR9cjadtysQCFFs03NNLtgGjBSpvkKHo+BO0v31DN7WUT1iDNoCwREPNa+0VEBOOBzd8ZKsKE1PqKF+COdcw/RTqwgUcBkI+h4a4sHtSA88ags7Dw/SOL2GKKplz3sL3lbinOpHMBvqJZ16mN1LIdF+VcaN5IJEFQ0aTud0j2W38N//uoIDs/UKJjm3D6udD4ZeLNqUUN3iHtx0gVdPRHTZS5m/rEqdn0lyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwLxvJ42QhCEFC6y3LME7EqMBFrOYOEpAfqH5BL/AYK8F+iQWwtV6uPgdz2kQZeCcpdxuKJk+b67PvMy08Nrx/Bw=="
}
]
},
{
"header": {
"sequence": 4,
"previousBlockHash": "068264A6E2174F72920376BCC86A684365995FD78393962AACD911D0A185CB35",
"noteCommitment": {
"type": "Buffer",
"data": "base64:iyOJ7SX7LysRDvR4I/rf3E0PgKzJ7/LycBFe/2WIHwA="
},
"transactionCommitment": {
"type": "Buffer",
"data": "base64:nwkMluSdhPxVwiKuWAGOuSiGxpkU7CNfqEZI3skVJNM="
},
"target": "878277375889837647326843029495509009809390053592540685978895509768758568",
"randomness": "0",
"timestamp": 1682349765282,
"graffiti": "0000000000000000000000000000000000000000000000000000000000000000",
"noteSize": 6,
"work": "0"
},
"transactions": [
{
"type": "Buffer",
"data": "base64:AQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGzKiP////8AAAAArLq5t/l+qHE6E3Ti2WlC0LqJMr7eMiWowoKWH+EOItuN0V8qv87MC459OeBEFM/U/UJhejy7VlqhMuzz2v0GZritdB09K5QqnA8/9iVQaoWz7s5fbpEllPRetMSC3VweBltm0P/I2BTefIV1AiFJLoWU+Ob2UcEm4nG3Xe2aYfYReniGPVkeo5ysyxUJcKUSl8Wtz/MsgeVsEHTUb60URiAtKCRhw4DD/XT4kA91BuqSWrW0sI97K+1ZKtSjR+2OH9q5OTykIKkTmhOyPQTzFT/RsmKwgImaX+6A6qaAVd0jA4bDTN9tqzeBOx41AJt+XVFuTBe4zdbehcypBNVS63/dlqcAdJ0OqtiQYexQ2o2VeR4SHwM5VTwjFsm7oopaEWQAGWACu7a6lXOn1xdMgTN9vKExh4pqPhKov2pTNqBvc1acw6FkpOsVpjCcHBj1fmrriynq+wnDpFlT+AHvbpHvejyWH6gzi80SgP6+iHxu8KgqJ1GsK8OxRqKQzslbPB2PUJn/gD5ZCw9hwdbp5+phaRLk+GCmU6OY7vYK453yJiCTDsRSMUJSFK27n9tEwfXa4GMb7mH1CpaV9zqU28RsUaZqZ4jLs9Q7yMTB/yLQaXyFZbpptUlyb24gRmlzaCBub3RlIGVuY3J5cHRpb24gbWluZXIga2V5MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwxopeq6V3mB0RggjNHGEIGmG0Z5mz05Ykf0lyMV9AQu1moly5ibBoyiBRJtM9Atb8c9GSI7epEE0haiwlSXuQCw=="
}
]
}
]
}
38 changes: 38 additions & 0 deletions ironfish/src/rpc/routes/wallet/createTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Asset } from '@ironfish/rust-nodejs'
import { RawTransactionSerde } from '../../../primitives/rawTransaction'
import { useAccountFixture, useMinerBlockFixture } from '../../../testUtilities'
import { createRouteTest } from '../../../testUtilities/routeTest'
import { AsyncUtils } from '../../../utils'
import { ERROR_CODES } from '../../adapters/errors'

const REQUEST_PARAMS = {
Expand Down Expand Up @@ -383,4 +384,41 @@ describe('Route wallet/createTransaction', () => {
}),
)
})

it('should generate a valid transaction by spending the specified notes', async () => {
const sender = await useAccountFixture(routeTest.node.wallet, 'existingAccount')

for (let i = 0; i < 3; ++i) {
const block = await useMinerBlockFixture(
routeTest.chain,
undefined,
sender,
routeTest.node.wallet,
)

await expect(routeTest.node.chain).toAddBlock(block)

await routeTest.node.wallet.updateHead()
}

const decryptedNotes = await AsyncUtils.materialize(sender.getNotes())
const notes = decryptedNotes.map((note) => note.note.hash().toString('hex'))

const requestParams = { ...REQUEST_PARAMS, notes }

const response = await routeTest.client.wallet.createTransaction(requestParams)

expect(response.status).toBe(200)
expect(response.content.transaction).toBeDefined()

const rawTransactionBytes = Buffer.from(response.content.transaction, 'hex')
const rawTransaction = RawTransactionSerde.deserialize(rawTransactionBytes)

expect(rawTransaction.outputs.length).toBe(1)
expect(rawTransaction.expiration).toBeDefined()
expect(rawTransaction.burns.length).toBe(0)
expect(rawTransaction.mints.length).toBe(0)
expect(rawTransaction.spends.length).toBe(3)
expect(rawTransaction.fee).toBe(1n)
})
})
9 changes: 9 additions & 0 deletions ironfish/src/rpc/routes/wallet/createTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type CreateTransactionRequest = {
expiration?: number
expirationDelta?: number
confirmations?: number
notes?: string[]
}

export type CreateTransactionResponse = {
Expand Down Expand Up @@ -83,6 +84,7 @@ export const CreateTransactionRequestSchema: yup.ObjectSchema<CreateTransactionR
expiration: yup.number().optional(),
expirationDelta: yup.number().optional(),
confirmations: yup.number().optional(),
notes: yup.array(yup.string().defined()).optional(),
})
.defined()

Expand Down Expand Up @@ -171,6 +173,13 @@ router.register<typeof CreateTransactionRequestSchema, CreateTransactionResponse
params.feeRate = node.memPool.feeEstimator.estimateFeeRate('average')
}

if (request.data.notes) {
params.notes = []
for (const noteHash of request.data.notes) {
params.notes.push(Buffer.from(noteHash, 'hex'))
}
}

try {
const transaction = await node.wallet.createTransaction(params)
const serialized = RawTransactionSerde.serialize(transaction)
Expand Down
Loading

0 comments on commit bb99849

Please sign in to comment.