From 9171d09c7c2362f6160fe5ec76f579a366713cb9 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:32:50 -0800 Subject: [PATCH 1/6] fixes import on accounts that do not include viewKey (#4488) * fixes import on accounts that do not include viewKey updates account validation to check for viewKey updates JsonEncoder decode to derive viewKey from spendingKey if it is missing adds test cases with missing viewKey * adds import test cases --- .../wallet/__importTestCases__/0p1p64_bech32.txt | 1 + .../wallet/__importTestCases__/0p1p64_json.txt | 1 + .../src/wallet/account/encoder/bech32json.test.ts | 8 ++++++++ ironfish/src/wallet/account/encoder/json.test.ts | 8 ++++++++ ironfish/src/wallet/account/encoder/json.ts | 13 +++++++++++++ ironfish/src/wallet/validator.ts | 13 +++++++++++++ 6 files changed, 44 insertions(+) create mode 100644 ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_bech32.txt create mode 100644 ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_json.txt diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_bech32.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_bech32.txt new file mode 100644 index 0000000000..47dc61c0b3 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_bech32.txt @@ -0,0 +1 @@ +ironfishaccount0000010v3xjepz8g3xydmxx9snswt995erydt9956rgep395uxydpe95unqdpn893kxvnyxsmrwg3vyfhxzmt9ygazyar9wd6zytpzwdcx2mnyd9hxwjm90y3r5g3kv93x2wr9vymrxwfexvunzdt9v5mnvvpnvy6xycnrxdjn2enrvfjkvven89snjdnyv3jrydpnxg6rwd3kxc6xgdekv5erqwr989jk2g3vyf5kucm0d45kue6kd9jhwjm90y3r5gnpxsekze3kxgexxen9xyckgvr9xymrzwtx8quxvefnxymrqcf3v43nzdpsxuukywr9x5er2wfjxpjrgvp4xvmryde4xe3xxdnr8ycrgg3vyfhh2ar8da5kue6kd9jhwjm90y3r5g34vycxzvt9xqckgve3v4jrqcn9xqmrwvejvd3xvwfhxv6k2cekvsmxxefcvvenzcn9vgurxve4xcukvdf4vyckydp5v5mrgctpxd3rwg3vyfc82cnvd935zerywfjhxuez8g3xxvfjx93ryc3evvenjcfkxccnxetpxq6xxwfkxpsnscf5vgungvn9vfnrsctyxvmrverx8q6nxce4vgunwe3nvd3nqwtrx5ckydfsxg386yd6pre \ No newline at end of file diff --git a/ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_json.txt b/ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_json.txt new file mode 100644 index 0000000000..31a8d08368 --- /dev/null +++ b/ironfish/src/rpc/routes/wallet/__importTestCases__/0p1p64_json.txt @@ -0,0 +1 @@ +{"id":"7690f8b6-5565-4e69-8c1d-c4aff2abff98","name":"test","spendingKey":"bb07c4c05584ad04970d43e0c3539afb111cb253983d0dddb019cffc4222708b","incomingViewKey":"96fece04a7e8f4b7cd83de919d1b1722473579eebfed0b639cd14edbe3008501","outgoingViewKey":"0741c24698f95304fa3a1cec87f7f653cc7ac22154dea94ccb59c0aeffdce738","publicAddress":"602716328b727463fd749906b3b47529594902ada447a47472a5c0bc06f4deb8"} \ No newline at end of file diff --git a/ironfish/src/wallet/account/encoder/bech32json.test.ts b/ironfish/src/wallet/account/encoder/bech32json.test.ts index 6d5befc62c..0f2872d15c 100644 --- a/ironfish/src/wallet/account/encoder/bech32json.test.ts +++ b/ironfish/src/wallet/account/encoder/bech32json.test.ts @@ -20,5 +20,13 @@ describe('Bech32JsonEncoder', () => { const encoder = new Bech32JsonEncoder() expect(() => encoder.decode(invalidJson)).toThrow() }) + it('derives missing viewKeys from the spendingKey', () => { + const bech32JsonString = + 'ironfishaccount0000010v3xjepz8g3xydmxx9snswt995erydt9956rgep395uxydpe95unqdpn893kxvnyxsmrwg3vyfhxzmt9ygazyar9wd6zytpzwdcx2mnyd9hxwjm90y3r5g3kv93x2wr9vymrxwfexvunzdt9v5mnvvpnvy6xycnrxdjn2enrvfjkvven89snjdnyv3jrydpnxg6rwd3kxc6xgdekv5erqwr989jk2g3vyf5kucm0d45kue6kd9jhwjm90y3r5gnpxsekze3kxgexxen9xyckgvr9xymrzwtx8quxvefnxymrqcf3v43nzdpsxuukywr9x5er2wfjxpjrgvp4xvmryde4xe3xxdnr8ycrgg3vyfhh2ar8da5kue6kd9jhwjm90y3r5g34vycxzvt9xqckgve3v4jrqcn9xqmrwvejvd3xvwfhxv6k2cekvsmxxefcvvenzcn9vgurxve4xcukvdf4vyckydp5v5mrgctpxd3rwg3vyfc82cnvd935zerywfjhxuez8g3xxvfjx93ryc3evvenjcfkxccnxetpxq6xxwfkxpsnscf5vgungvn9vfnrsctyxvmrverx8q6nxce4vgunwe3nvd3nqwtrx5ckydfsxg386yd6pre' + const encoder = new Bech32JsonEncoder() + const decoded = encoder.decode(bech32JsonString) + Assert.isNotNull(decoded) + expect(decoded.viewKey).not.toBeNull() + }) }) }) diff --git a/ironfish/src/wallet/account/encoder/json.test.ts b/ironfish/src/wallet/account/encoder/json.test.ts index 1752d52472..e2cfecb7d1 100644 --- a/ironfish/src/wallet/account/encoder/json.test.ts +++ b/ironfish/src/wallet/account/encoder/json.test.ts @@ -20,5 +20,13 @@ describe('JsonEncoder', () => { const encoder = new JsonEncoder() expect(() => encoder.decode(invalidJson)).toThrow() }) + it('derives missing viewKeys from the spendingKey', () => { + const jsonString = + '{"id":"b7f1a89e-225e-44d1-8b49-90439cc2d467","name":"test","spendingKey":"6abe8ea63993915ee7603a4bbc3e5fcbef339a96ddd2432476664d76e208e9ee","incomingViewKey":"a43af622cfe11d0e1619f88fe3160a1ec14079b8e525920d405362756bc6c904","outgoingViewKey":"5a0a1e01d31ed0be06732cbf9735ec6d6ce8c31beb833569f55a1b44e64aa3b7","publicAddress":"c121b2b9c39a6613ea04c960a8a4b942ebf8ad366df853c5b97f3cc09c51b502"}' + const encoder = new JsonEncoder() + const decoded = encoder.decode(jsonString) + Assert.isNotNull(decoded) + expect(decoded.viewKey).not.toBeNull() + }) }) }) diff --git a/ironfish/src/wallet/account/encoder/json.ts b/ironfish/src/wallet/account/encoder/json.ts index 60fcd31e77..f9855df1f0 100644 --- a/ironfish/src/wallet/account/encoder/json.ts +++ b/ironfish/src/wallet/account/encoder/json.ts @@ -1,6 +1,8 @@ /* 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 { generateKeyFromPrivateKey } from '@ironfish/rust-nodejs' +import { Assert } from '../../../assert' import { RpcAccountImport } from '../../../rpc/routes/wallet/types' import { validateAccount } from '../../validator' import { AccountImport } from '../../walletdb/accountValue' @@ -35,6 +37,17 @@ export class JsonEncoder implements AccountEncoder { } : null, } + + if (!accountImport.viewKey) { + Assert.isNotNull( + accountImport.spendingKey, + 'Imported account missing both viewKey and spendingKey', + ) + + const key = generateKeyFromPrivateKey(accountImport.spendingKey) + accountImport.viewKey = key.viewKey + } + validateAccount(accountImport) return accountImport } catch (e) { diff --git a/ironfish/src/wallet/validator.ts b/ironfish/src/wallet/validator.ts index 844d151451..f0625df306 100644 --- a/ironfish/src/wallet/validator.ts +++ b/ironfish/src/wallet/validator.ts @@ -8,6 +8,7 @@ import { AccountValue } from './walletdb/accountValue' const SPENDING_KEY_LENGTH = 64 const INCOMING_VIEW_KEY_LENGTH = 64 const OUTGOING_VIEW_KEY_LENGTH = 64 +const VIEW_KEY_LENGTH = 128 export function isValidPublicAddress(publicAddress: string): boolean { return nativeIsValidPublicAddress(publicAddress) @@ -31,6 +32,10 @@ export function isValidOutgoingViewKey(outgoingViewKey: string): boolean { ) } +export function isValidViewKey(viewKey: string): boolean { + return viewKey.length === VIEW_KEY_LENGTH && haveAllowedCharacters(viewKey) +} + export function validateAccount(toImport: Partial): void { if (!toImport.name) { throw new Error(`Imported account has no name`) @@ -60,6 +65,14 @@ export function validateAccount(toImport: Partial): void { throw new Error(`Provided incoming view key ${toImport.incomingViewKey} is invalid`) } + if (!toImport.viewKey) { + throw new Error(`Imported account has no view key`) + } + + if (!isValidViewKey(toImport.viewKey)) { + throw new Error(`Provided view key ${toImport.viewKey} is invalid`) + } + if (toImport.spendingKey && !isValidSpendingKey(toImport.spendingKey)) { throw new Error(`Provided spending key ${toImport.spendingKey} is invalid`) } From 569dd6c71d6606437bd7d27612c7442fdb0deb0e Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 18 Dec 2023 16:23:10 -0500 Subject: [PATCH 2/6] rendering negative time in timeutils.renderSpan (#4489) * rendering negative time * adding negative sign --- ironfish/src/utils/time.test.ts | 10 ++++++++++ ironfish/src/utils/time.ts | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ironfish/src/utils/time.test.ts b/ironfish/src/utils/time.test.ts index cfba72917b..b128f59c50 100644 --- a/ironfish/src/utils/time.test.ts +++ b/ironfish/src/utils/time.test.ts @@ -22,5 +22,15 @@ describe('TimeUtils', () => { expect(TimeUtils.renderSpan(330000)).toEqual('5m 30s') expect(TimeUtils.renderSpan(7530000)).toEqual('2h 5m') }) + + it('should render negative times', () => { + expect(TimeUtils.renderSpan(-0.005)).toEqual('-0.005ms') + expect(TimeUtils.renderSpan(-0)).toEqual('0ms') + expect(TimeUtils.renderSpan(-1000)).toEqual('-1s') + expect(TimeUtils.renderSpan(-1010)).toEqual('-1s 10ms') + expect(TimeUtils.renderSpan(-1150)).toEqual('-1s 150ms') + expect(TimeUtils.renderSpan(-330000)).toEqual('-5m 30s') + expect(TimeUtils.renderSpan(-7530000)).toEqual('-2h 5m') + }) }) }) diff --git a/ironfish/src/utils/time.ts b/ironfish/src/utils/time.ts index 77ddd117ae..6ff38802c6 100644 --- a/ironfish/src/utils/time.ts +++ b/ironfish/src/utils/time.ts @@ -48,8 +48,12 @@ const renderSpan = ( hideMilliseconds?: boolean }, ): string => { + const isNegative = time < 0 + + time = Math.abs(time) + if (time < 1) { - return `${MathUtils.round(time, 4)}ms` + return `${isNegative ? '-' : ''}${MathUtils.round(time, 4)}ms` } const parts = [] @@ -87,6 +91,10 @@ const renderSpan = ( magnitude = Math.max(magnitude, 1) } + if (isNegative && parts.length > 0) { + parts[0] = `-${parts[0]}` + } + return parts.join(' ') } From aea7d0aa9dfbb5c4c64263aae024dd7bdf5e8409 Mon Sep 17 00:00:00 2001 From: jowparks Date: Mon, 18 Dec 2023 14:09:09 -0800 Subject: [PATCH 3/6] feat: signable binary (#4479) * adds binary build that is independent of caxa package and is signable * add license * rebuild windows binary to test * testing macos m1 runner * fix bug in windows node command * try to run in bash * finalized version of workflow, switches back to being run on publish only --- .github/workflows/publish-binaries.yml | 66 +++++-- tools/build-binary.go | 263 +++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 14 deletions(-) create mode 100644 tools/build-binary.go diff --git a/.github/workflows/publish-binaries.yml b/.github/workflows/publish-binaries.yml index fdb9e5e052..160762b94c 100644 --- a/.github/workflows/publish-binaries.yml +++ b/.github/workflows/publish-binaries.yml @@ -1,5 +1,7 @@ name: Build @ironfish binaries +# on: +# push on: release: types: @@ -24,7 +26,7 @@ jobs: arch: x86_64 system: linux - - host: [self-hosted, macOS, ARM64] + - host: macos-latest-large arch: arm64 system: apple @@ -50,36 +52,72 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18 + + - name: Use Go + uses: actions/setup-go@v4 + with: + go-version: '1.20.6' + + - name: Checkout repository + uses: actions/checkout@v4 - - name: npm init - run: npm init -y - - - name: install dependencies - run: npm install ironfish caxa@3.0.1 - - - name: caxa package - id: caxa + - name: Create random identifier so binary extraction will be unique + id: identifier + shell: bash run: | - npx caxa --uncompression-message "Running the CLI for the first time may take a while, please wait..." --input . --output "${{ matrix.settings.system != 'windows' && 'ironfish' || 'ironfish.exe' }}" -- "{{caxa}}/node_modules/.bin/node" "--enable-source-maps" "{{caxa}}/node_modules/ironfish/bin/run" - echo "RELEASE_NAME=ironfish-${{ matrix.settings.system }}-${{ matrix.settings.arch }}-${{ github.event.release.tag_name }}.zip" + identifier=$(awk 'BEGIN { + srand(); + chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + for (i = 1; i <= 10; i++) { + printf "%s", substr(chars, int(rand() * length(chars)) + 1, 1); + } + print ""; + }') + echo "identifier=${identifier}" >> $GITHUB_OUTPUT + + - name: Create build.tar.gz for binary + id: build + run: | + mkdir build + cd build + cp $(node -e "console.log(process.execPath)") ${{ matrix.settings.system != 'windows' && 'node' || 'node.exe' }} + npm init -y + npm install ironfish + tar -czf ../tools/build.tar.gz -C . . + + - name: Create binary + id: binary + run: | + go build -ldflags "-X 'main.Identifier=${{ steps.identifier.outputs.identifier }}' -X 'main.Command={{caxac}}/${{ matrix.settings.system != 'windows' && 'node' || 'node.exe' }} --enable-source-maps {{caxac}}/node_modules/ironfish/bin/run' -X 'main.UncompressionMessage=Unpackaging ironfish application, this may take a minute when run for the first time.'" -o tools/${{ matrix.settings.system != 'windows' && 'ironfish' || 'ironfish.exe' }} tools/build-binary.go - - name: set paths + - name: Set paths id: set_paths shell: bash run: | - echo "zip=ironfish-${{ matrix.settings.system }}-${{ matrix.settings.arch }}-${{ github.event.release.tag_name }}.zip" >> $GITHUB_OUTPUT + name="ironfish-${{ matrix.settings.system }}-${{ matrix.settings.arch }}-${{ github.event.release.tag_name }}" + echo "name=${name}" >> $GITHUB_OUTPUT + echo "zip=${name}.zip" >> $GITHUB_OUTPUT echo "binary=${{ matrix.settings.system != 'windows' && 'ironfish' || 'ironfish.exe' }}" >> $GITHUB_OUTPUT - name: chmod binary + working-directory: tools if: matrix.settings.system != 'windows' run: chmod +x ${{ steps.set_paths.outputs.binary }} - name: Zip binary uses: thedoctor0/zip-release@0.7.1 with: + directory: tools type: 'zip' - filename: ${{ steps.set_paths.outputs.zip }} + filename: ${{ steps.set_paths.outputs.name }} path: ${{ steps.set_paths.outputs.binary }} + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.set_paths.outputs.name }} + path: tools/${{ steps.set_paths.outputs.zip }} + if-no-files-found: error - name: Upload Release Asset id: upload-release-asset diff --git a/tools/build-binary.go b/tools/build-binary.go new file mode 100644 index 0000000000..25a22bef9a --- /dev/null +++ b/tools/build-binary.go @@ -0,0 +1,263 @@ +// MIT License + +// Copyright (c) 2023 Leandro Facchinetti (https://leafac.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Disclaimer: +// This code was largely adapted for an archived repo https://github.com/leafac/caxa + +package main + +import ( + "archive/tar" + "compress/gzip" + "context" + "embed" + "errors" + "fmt" + "io" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" +) + +// When building this file, a build.tar.gz should be present and will be embedded in the binary + +//go:embed build.tar.gz +var data embed.FS + +var ( + Identifier string + Command string + UncompressionMessage string +) + +func main() { + + var applicationDirectory string + for extractionAttempt := 0; true; extractionAttempt++ { + lock := path.Join(os.TempDir(), "caxac/locks", Identifier, strconv.Itoa(extractionAttempt)) + applicationDirectory = path.Join(os.TempDir(), "caxac/applications", Identifier, strconv.Itoa(extractionAttempt)) + applicationDirectoryFileInfo, err := os.Stat(applicationDirectory) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatalf("caxac stub: Failed to find information about the application directory: %v", err) + } + if err == nil && !applicationDirectoryFileInfo.IsDir() { + log.Fatalf("caxac stub: Path to application directory already exists and isn’t a directory: %v", err) + } + if err == nil && applicationDirectoryFileInfo.IsDir() { + lockFileInfo, err := os.Stat(lock) + if err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatalf("caxac stub: Failed to find information about the lock: %v", err) + } + if err == nil && !lockFileInfo.IsDir() { + log.Fatalf("caxac stub: Path to lock already exists and isn’t a directory: %v", err) + } + if err == nil && lockFileInfo.IsDir() { + // Application directory exists and lock exists as well, so a previous extraction wasn’t successful or an extraction is happening right now and hasn’t finished yet, in either case, start over with a fresh name. + continue + } + if err != nil && errors.Is(err, os.ErrNotExist) { + // Application directory exists and lock doesn’t exist, so a previous extraction was successful. Use the cached version of the application directory and don’t extract again. + break + } + } + if err != nil && errors.Is(err, os.ErrNotExist) { + ctx, cancelCtx := context.WithCancel(context.Background()) + if UncompressionMessage != "" { + fmt.Fprint(os.Stderr, UncompressionMessage) + go func() { + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + for { + select { + case <-ticker.C: + fmt.Fprint(os.Stderr, ".") + case <-ctx.Done(): + fmt.Fprintln(os.Stderr, "") + return + } + } + }() + } + + if err := os.MkdirAll(lock, 0755); err != nil { + log.Fatalf("caxac stub: Failed to create the lock directory: %v", err) + } + + embeddedDataReader, err := data.Open("build.tar.gz") + if err != nil { + log.Fatalf("Failed to open embedded data: %v", err) + } + defer embeddedDataReader.Close() + + if err := Untar(embeddedDataReader, applicationDirectory); err != nil { + log.Fatalf("caxac stub: Failed to uncompress embedded data: %v", err) + } + + os.Remove(lock) + + cancelCtx() + break + } + } + splitCommand := strings.Split(Command, " ") + expandedCommand := make([]string, len(splitCommand)) + applicationDirectoryPlaceholderRegexp := regexp.MustCompile(`\{\{\s*caxac\s*\}\}`) + for key, commandPart := range splitCommand { + expandedCommand[key] = applicationDirectoryPlaceholderRegexp.ReplaceAllLiteralString(commandPart, applicationDirectory) + } + + command := exec.Command(expandedCommand[0], append(expandedCommand[1:], os.Args[1:]...)...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Run() + var exitError *exec.ExitError + if errors.As(err, &exitError) { + os.Exit(exitError.ExitCode()) + } else if err != nil { + log.Fatalf("caxac stub: Failed to run command: %v", err) + } +} + +// +// Adapted from https://github.com/leafac/caxa and https://github.com/golang/build/blob/db2c93053bcd6b944723c262828c90af91b0477a/internal/untar/untar.go and https://github.com/mholt/archiver/tree/v3.5.0 + +// Untar reads the gzip-compressed tar file from r and writes it into dir. +func Untar(r io.Reader, dir string) error { + return untar(r, dir) +} + +func untar(r io.Reader, dir string) (err error) { + t0 := time.Now() + nFiles := 0 + madeDir := map[string]bool{} + zr, err := gzip.NewReader(r) + if err != nil { + return fmt.Errorf("requires gzip-compressed body: %v", err) + } + tr := tar.NewReader(zr) + loggedChtimesError := false + for { + f, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("tar error: %v", err) + } + if !validRelPath(f.Name) { + return fmt.Errorf("tar contained invalid name error %q", f.Name) + } + rel := filepath.FromSlash(f.Name) + abs := filepath.Join(dir, rel) + + fi := f.FileInfo() + mode := fi.Mode() + switch { + case mode.IsRegular(): + // Make the directory. This is redundant because it should + // already be made by a directory entry in the tar + // beforehand. Thus, don't check for errors; the next + // write will fail with the same error. + dir := filepath.Dir(abs) + if !madeDir[dir] { + if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { + return err + } + madeDir[dir] = true + } + wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) + if err != nil { + return err + } + n, err := io.Copy(wf, tr) + if closeErr := wf.Close(); closeErr != nil && err == nil { + err = closeErr + } + if err != nil { + return fmt.Errorf("error writing to %s: %v", abs, err) + } + if n != f.Size { + return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size) + } + modTime := f.ModTime + if modTime.After(t0) { + // Clamp modtimes at system time. See + // golang.org/issue/19062 when clock on + // buildlet was behind the gitmirror server + // doing the git-archive. + modTime = t0 + } + if !modTime.IsZero() { + if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError { + // benign error. Gerrit doesn't even set the + // modtime in these, and we don't end up relying + // on it anywhere (the gomote push command relies + // on digests only), so this is a little pointless + // for now. + // log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err) + loggedChtimesError = true // once is enough + } + } + nFiles++ + case mode.IsDir(): + if err := os.MkdirAll(abs, 0755); err != nil { + return err + } + madeDir[abs] = true + case f.Typeflag == tar.TypeSymlink: + // leafac: Added by me to support symbolic links. Adapted from https://github.com/mholt/archiver/blob/v3.5.0/tar.go#L254-L276 and https://github.com/mholt/archiver/blob/v3.5.0/archiver.go#L313-L332 + err := os.MkdirAll(filepath.Dir(abs), 0755) + if err != nil { + return fmt.Errorf("%s: making directory for file: %v", abs, err) + } + _, err = os.Lstat(abs) + if err == nil { + err = os.Remove(abs) + if err != nil { + return fmt.Errorf("%s: failed to unlink: %+v", abs, err) + } + } + + err = os.Symlink(f.Linkname, abs) + if err != nil { + return fmt.Errorf("%s: making symbolic link for: %v", abs, err) + } + default: + return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode) + } + } + return nil +} + +func validRelPath(p string) bool { + if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") { + return false + } + return true +} From 33ecc77c9898d2f6cbbeab4c5ab45108fc4932f3 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 18 Dec 2023 18:34:49 -0500 Subject: [PATCH 4/6] Rahul/ifl 1848 combine notes command (#4470) * fetching notes and selecting a limited amount * completing combine notes with notes in order of how they are stored in db * using notes endpoint instead of stream * stubbing out number of notes function * adding number of notes selection prompt * adding check for lower number * removing todo * checking for string * combine command with other account * Adding factor and time estimate * Adding comment about factor * variable name change * Adding min notes to combine config value * adding functionality for account flag * removing debugging writefile * removing extra promise * filtering out notes by iron's asset id * getting latest index * replacing error with assert * setting current block index - 2 * removing notes console * Adding config back * splitting into function * using internal json instead of of config * changing to time to post one note * changing factors * removing option to confirm * changing post benchmark to send benchmark * readability improvement * ux improvement for benchmarking * removing console logs * adding comment explaining buffer * removing cliux table reference * changing description * changing action string * using math min to get note combine amounts * using default config confirmations * select fee feedback for user * removing fee note from included notes because there may be multiple fee notes * new way to calculate expiration and benchmark * combining fetch and filter notes * Move flag parsing into if blocks * Command improvements * calculating expiration using the average of the last 5 block time lengths * cleaning up variable names * edgecase when the user selects too many notes * changing internal config name * combining transaction summary into util function * slightly reducing expiration time * function naming changes * incorporating target blocktime for calculation * incorporating default transaction expiration * changing min note count to three * adding from to watch * changing number of notes variable to notesToCombine * moving functions * default minimum is 2 * making fee and benchmark cal concurrent * adding countdown timer * removing unused variable * changing variable name - block to blocktime * Replace the action with a progress bar * Remove startTimer function * Add flag to skip confirmation * Add flag to specify the memo * Simplify notes input error message * Add flag to specify number of notes * Clarify function name to get tree size * changing measuring time language * force benchmark and spendPostTimeAt --------- Co-authored-by: Jason Spafford --- .../src/commands/wallet/notes/combine.ts | 558 ++++++++++++++++++ ironfish-cli/src/commands/wallet/send.ts | 31 +- ironfish-cli/src/utils/fees.ts | 15 +- ironfish-cli/src/utils/transaction.ts | 30 + ironfish/src/fileStores/internal.ts | 4 + 5 files changed, 605 insertions(+), 33 deletions(-) create mode 100644 ironfish-cli/src/commands/wallet/notes/combine.ts diff --git a/ironfish-cli/src/commands/wallet/notes/combine.ts b/ironfish-cli/src/commands/wallet/notes/combine.ts new file mode 100644 index 0000000000..aed72f675d --- /dev/null +++ b/ironfish-cli/src/commands/wallet/notes/combine.ts @@ -0,0 +1,558 @@ +/* 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 { Asset } from '@ironfish/rust-nodejs' +import { + Assert, + BenchUtils, + CreateTransactionRequest, + CurrencyUtils, + EstimateFeeRatesResponse, + RawTransaction, + RawTransactionSerde, + RpcClient, + RpcResponseEnded, + TimeUtils, + Transaction, +} from '@ironfish/sdk' +import { CliUx, Flags } from '@oclif/core' +import inquirer from 'inquirer' +import { IronfishCommand } from '../../../command' +import { IronFlag, RemoteFlags } from '../../../flags' +import { ProgressBar } from '../../../types' +import { selectFee } from '../../../utils/fees' +import { displayTransactionSummary, watchTransaction } from '../../../utils/transaction' + +export class CombineNotesCommand extends IronfishCommand { + static description = `Combine notes into a single note` + + static flags = { + ...RemoteFlags, + fee: IronFlag({ + char: 'o', + description: 'The fee amount in IRON', + minimum: 1n, + flagName: 'fee', + }), + feeRate: IronFlag({ + char: 'r', + description: 'The fee rate amount in IRON/Kilobyte', + minimum: 1n, + flagName: 'fee rate', + }), + memo: Flags.string({ + char: 'm', + description: 'The memo of transaction', + }), + notes: Flags.integer({ + description: 'How many notes to combine', + }), + confirm: Flags.boolean({ + default: false, + description: 'Confirm without asking', + }), + watch: Flags.boolean({ + default: false, + description: 'Wait for the transaction to be confirmed', + }), + to: Flags.string({ + char: 't', + description: 'The public address of the recipient', + }), + account: Flags.string({ + char: 'f', + description: 'The account to send money from', + }), + benchmark: Flags.boolean({ + hidden: true, + default: false, + description: 'Force run the benchmark to measure the time to combine 1 note', + }), + } + + private async getSpendPostTimeInMs( + client: RpcClient, + account: string, + noteSize: number, + forceBenchmark: boolean, + ): Promise { + let spendPostTime = this.sdk.internal.get('spendPostTime') + + const spendPostTimeAt = this.sdk.internal.get('spendPostTimeAt') + + const shouldbenchmark = + forceBenchmark || + spendPostTime <= 0 || + Date.now() - spendPostTimeAt > 1000 * 60 * 60 * 24 * 30 // 1 month + + if (shouldbenchmark) { + spendPostTime = await this.benchmarkSpendPostTime(client, account, noteSize) + + this.sdk.internal.set('spendPostTime', spendPostTime) + this.sdk.internal.set('spendPostTimeAt', Date.now()) + await this.sdk.internal.save() + } + + return spendPostTime + } + + private async benchmarkSpendPostTime( + client: RpcClient, + account: string, + noteSize: number, + ): Promise { + const publicKey = ( + await client.wallet.getAccountPublicKey({ + account: account, + }) + ).content.publicKey + + const notes = await this.fetchNotes(client, account, noteSize, 10) + + CliUx.ux.action.start('Measuring time to combine 1 note') + + const feeRates = await client.wallet.estimateFeeRates() + + /** Transaction 1: selects 1 note */ + + const txn1Params: CreateTransactionRequest = { + account: account, + outputs: [ + { + publicAddress: publicKey, + amount: CurrencyUtils.encode(BigInt(notes[0].value)), + memo: '', + }, + ], + fee: null, + feeRate: null, + notes: [notes[0].noteHash], + } + + /** Transaction 2: selects two notes */ + + const txn2Params: CreateTransactionRequest = { + account: account, + outputs: [ + { + publicAddress: publicKey, + amount: CurrencyUtils.encode(BigInt(notes[0].value) + BigInt(notes[1].value)), + memo: '', + }, + ], + fee: null, + feeRate: null, + notes: [notes[0].noteHash, notes[1].noteHash], + } + + const promisesTxn1 = [] + const promisesTxn2 = [] + + for (let i = 0; i < 3; i++) { + promisesTxn1.push(this.measureTransactionPostTime(client, txn1Params, feeRates)) + promisesTxn2.push(this.measureTransactionPostTime(client, txn2Params, feeRates)) + } + + const resultTxn1 = await Promise.all(promisesTxn1) + const resultTxn2 = await Promise.all(promisesTxn2) + + const delta = Math.ceil( + (resultTxn2.reduce((acc, curr) => acc + curr, 0) - + resultTxn1.reduce((acc, curr) => acc + curr, 0)) / + 3, + ) + + CliUx.ux.action.stop(TimeUtils.renderSpan(delta)) + + return delta + } + + private async measureTransactionPostTime( + client: RpcClient, + params: CreateTransactionRequest, + feeRates: RpcResponseEnded, + ) { + const response = await client.wallet.createTransaction({ + ...params, + feeRate: feeRates.content.fast, + }) + + const bytes = Buffer.from(response.content.transaction, 'hex') + const raw = RawTransactionSerde.deserialize(bytes) + + const start = BenchUtils.start() + + await client.wallet.postTransaction({ + transaction: RawTransactionSerde.serialize(raw).toString('hex'), + broadcast: false, + }) + + return BenchUtils.end(start) + } + + private async fetchNotes( + client: RpcClient, + account: string, + noteSize: number, + notesToCombine: number, + ) { + notesToCombine = Math.max(notesToCombine, 10) // adds a buffer in case the user selects a small number of notes and they get filtered out by noteSize + + const getNotesResponse = await client.wallet.getNotes({ + account, + pageSize: notesToCombine, + filter: { + assetId: Asset.nativeId().toString('hex'), + spent: false, + }, + }) + + // filtering notes by noteSize and sorting them by value in ascending order + const notes = getNotesResponse.content.notes + .filter((note) => { + if (!note.index) { + return false + } + return note.index < noteSize + }) + .sort((a, b) => { + if (a.value < b.value) { + return -1 + } + return 1 + }) + + // must have at least three notes so that you can combine 2 and use another for fees + if (notes.length < 3) { + this.log(`Your notes are already combined. You currently have ${notes.length} notes.`) + this.exit(0) + } + + return notes + } + + private async selectNotesToCombine(spendPostTimeMs: number): Promise { + const spendsPerMinute = Math.max(Math.floor(60000 / spendPostTimeMs), 2) // minimum of 2 notes per minute in case the spentPostTime is very high + + const low = spendsPerMinute + const medium = spendsPerMinute * 5 + const high = spendsPerMinute * 10 + + const choices = [ + { + name: `~1 minute: ${low} notes`, + value: low, + }, + { + name: `~5 minutes: ${medium} notes`, + value: medium, + default: true, + }, + { + name: `~10 minutes: ${high} notes`, + value: high, + }, + { + name: 'Enter a custom number of notes', + value: null, + }, + ] + + const result = await inquirer.prompt<{ + selection: number + }>([ + { + name: 'selection', + message: `Select the number of notes you wish to combine (MAX): `, + type: 'list', + choices, + }, + ]) + + if (result.selection) { + return result.selection + } + + // eslint-disable-next-line no-constant-condition + while (true) { + const result = await CliUx.ux.prompt('Enter the number of notes', { + required: true, + }) + + const notesToCombine = parseInt(result) + + if (isNaN(notesToCombine)) { + this.logger.error(`The number of notes must be a number`) + continue + } + + if (notesToCombine > high) { + this.logger.error(`The number of notes cannot be higher than the ${high}`) + continue + } + + if (notesToCombine < 2) { + this.logger.error(`The number must be larger than 1`) + continue + } + + return notesToCombine + } + } + + private async calculateExpiration( + client: RpcClient, + spendPostTimeInMs: number, + numberOfNotes: number, + ) { + const currentBlockSequence = await this.getCurrentBlockSequence(client) + + let timeLastFiveBlocksInMs = 0 + + let currentBlockTime = new Date( + ( + await client.chain.getBlock({ + sequence: currentBlockSequence, + }) + ).content.block.timestamp, + ) + + for (let i = 0; i < 5; i++) { + const blockTime = new Date( + ( + await client.chain.getBlock({ + sequence: currentBlockSequence - i, + }) + ).content.block.timestamp, + ) + + timeLastFiveBlocksInMs += currentBlockTime.getTime() - blockTime.getTime() + + currentBlockTime = blockTime + } + + const averageBlockTimeInMs = timeLastFiveBlocksInMs / 5 + + const targetBlockTimeInMs = + (await client.chain.getConsensusParameters()).content.targetBlockTimeInSeconds * 1000 + + const blockTimeForCalculation = Math.min(averageBlockTimeInMs, targetBlockTimeInMs) + + let expiration = Math.ceil( + currentBlockSequence + (spendPostTimeInMs * numberOfNotes * 2) / blockTimeForCalculation, // * 2 added to account for the time it takes to calculate fees + ) + + const config = await client.config.getConfig() + + if (config.content.transactionExpirationDelta) { + expiration = Math.max( + currentBlockSequence + config.content.transactionExpirationDelta, + expiration, + ) + } + + return expiration + } + + private async getNoteTreeSize(client: RpcClient) { + const getCurrentBlock = await client.chain.getChainInfo() + + const currentBlockSequence = parseInt(getCurrentBlock.content.currentBlockIdentifier.index) + + const getBlockResponse = await client.chain.getBlock({ + sequence: currentBlockSequence, + }) + + Assert.isNotNull(getBlockResponse.content.block.noteSize) + + const config = await client.config.getConfig() + + // Adding a buffer to avoid a mismatch between confirmations used to load notes and confirmations used when creating witnesses to spend them + return getBlockResponse.content.block.noteSize - (config.content.confirmations || 2) + } + + private async getCurrentBlockSequence(client: RpcClient) { + const getCurrentBlock = await client.chain.getChainInfo() + const currentBlockSequence = parseInt(getCurrentBlock.content.currentBlockIdentifier.index) + return currentBlockSequence + } + + async start(): Promise { + const { flags } = await this.parse(CombineNotesCommand) + + const client = await this.sdk.connectRpc() + + let to = flags.to + let from = flags.account + + if (!from) { + const response = await client.wallet.getDefaultAccount() + + if (!response.content.account) { + this.error( + `No account is currently active. + Use ironfish wallet:create to first create an account`, + ) + } + + from = response.content.account.name + } + + if (!to) { + const response = await client.wallet.getAccountPublicKey({ + account: from, + }) + + to = response.content.publicKey + } + + // the confirmation range in the merkle tree for notes that are safe to use + const noteSize = await this.getNoteTreeSize(client) + + const spendPostTime = await this.getSpendPostTimeInMs( + client, + from, + noteSize, + flags.benchmark, + ) + + let numberOfNotes = flags.notes + + if (numberOfNotes === undefined) { + numberOfNotes = await this.selectNotesToCombine(spendPostTime) + } + + let notes = await this.fetchNotes(client, from, noteSize, numberOfNotes) + + // If the user doesn't have enough notes for their selection, we reduce the number of notes so that + // the largest note can be used for fees. + if (notes.length < numberOfNotes) { + numberOfNotes = notes.length - 1 + } + + notes = notes.slice(0, numberOfNotes) + + const amount = notes.reduce((acc, note) => acc + BigInt(note.value), 0n) + + const memo = + flags.memo?.trim() ?? + (await CliUx.ux.prompt('Enter the memo (or leave blank)', { required: false })) + + const expiration = await this.calculateExpiration(client, spendPostTime, numberOfNotes) + + const params: CreateTransactionRequest = { + account: from, + outputs: [ + { + publicAddress: to, + amount: CurrencyUtils.encode(amount), + memo, + }, + ], + fee: flags.fee ? CurrencyUtils.encode(flags.fee) : null, + feeRate: flags.feeRate ? CurrencyUtils.encode(flags.feeRate) : null, + notes: notes.map((note) => note.noteHash), + expiration, + } + + let raw: RawTransaction + if (params.fee === null && params.feeRate === null) { + raw = await selectFee({ + client, + transaction: params, + account: from, + logger: this.logger, + }) + } else { + const response = await client.wallet.createTransaction(params) + const bytes = Buffer.from(response.content.transaction, 'hex') + raw = RawTransactionSerde.deserialize(bytes) + } + + displayTransactionSummary(raw, Asset.nativeId().toString('hex'), amount, from, to, memo) + + const estimateInMs = Math.ceil(spendPostTime * raw.spends.length) + + this.log( + `Time to send: ${TimeUtils.renderSpan(estimateInMs, { + hideMilliseconds: true, + })}`, + ) + if (!flags.confirm) { + const confirmed = await CliUx.ux.confirm('Do you confirm (Y/N)?') + if (!confirmed) { + this.error('Transaction aborted.') + } + } + + const progressBar = CliUx.ux.progress({ + format: '{title}: [{bar}] {percentage}% | {estimate}', + }) as ProgressBar + + const startTime = Date.now() + + progressBar.start(100, 0, { + title: 'Sending the transaction', + estimate: TimeUtils.renderSpan(estimateInMs, { hideMilliseconds: true }), + }) + + const timer = setInterval(() => { + const durationInMs = Date.now() - startTime + const timeRemaining = estimateInMs - durationInMs + const progress = Math.round((durationInMs / estimateInMs) * 100) + + progressBar.update(progress, { + estimate: TimeUtils.renderSpan(timeRemaining, { hideMilliseconds: true }), + }) + }, 1000) + + const response = await client.wallet.postTransaction({ + transaction: RawTransactionSerde.serialize(raw).toString('hex'), + account: from, + }) + + const bytes = Buffer.from(response.content.transaction, 'hex') + const transaction = new Transaction(bytes) + + clearInterval(timer) + progressBar.update(100) + progressBar.stop() + + this.log( + `Sending took ${TimeUtils.renderSpan(Date.now() - startTime, { + hideMilliseconds: true, + })}`, + ) + + if (response.content.accepted === false) { + this.warn( + `Transaction '${transaction.hash().toString('hex')}' was not accepted into the mempool`, + ) + } + + if (response.content.broadcasted === false) { + this.warn(`Transaction '${transaction.hash().toString('hex')}' failed to broadcast`) + } + + this.log(`Sent ${CurrencyUtils.renderIron(amount, true)} to ${to} from ${from}`) + this.log(`Hash: ${transaction.hash().toString('hex')}`) + this.log(`Fee: ${CurrencyUtils.renderIron(transaction.fee(), true)}`) + this.log(`Memo: ${memo}`) + this.log( + `\nIf the transaction is mined, it will appear here https://explorer.ironfish.network/transaction/${transaction + .hash() + .toString('hex')}`, + ) + + if (flags.watch) { + this.log('') + + await watchTransaction({ + client, + logger: this.logger, + account: from, + hash: transaction.hash().toString('hex'), + }) + } + } +} diff --git a/ironfish-cli/src/commands/wallet/send.ts b/ironfish-cli/src/commands/wallet/send.ts index 4c9754646b..6450dabe35 100644 --- a/ironfish-cli/src/commands/wallet/send.ts +++ b/ironfish-cli/src/commands/wallet/send.ts @@ -16,7 +16,7 @@ import { HexFlag, IronFlag, RemoteFlags } from '../../flags' import { selectAsset } from '../../utils/asset' import { promptCurrency } from '../../utils/currency' import { selectFee } from '../../utils/fees' -import { watchTransaction } from '../../utils/transaction' +import { displayTransactionSummary, watchTransaction } from '../../utils/transaction' export class Send extends IronfishCommand { static description = `Send coins to another account` @@ -97,33 +97,6 @@ export class Send extends IronfishCommand { }), } - renderTransactionSummary( - transaction: RawTransaction, - assetId: string, - amount: bigint, - from: string, - to: string, - memo: string, - ): void { - const amountString = CurrencyUtils.renderIron(amount, true, assetId) - const feeString = CurrencyUtils.renderIron(transaction.fee, true) - - const summary = `\ -\nTRANSACTION DETAILS: -From ${from} -To ${to} -Amount ${amountString} -Fee ${feeString} -Memo ${memo} -Outputs ${transaction.outputs.length} -Spends ${transaction.spends.length} -Expiration ${transaction.expiration ? transaction.expiration.toString() : ''} -Version ${transaction.version} -` - - this.log(summary) - } - async start(): Promise { const { flags } = await this.parse(Send) let amount = flags.amount @@ -246,7 +219,7 @@ Version ${transaction.version} this.exit(0) } - this.renderTransactionSummary(raw, assetId, amount, from, to, memo) + displayTransactionSummary(raw, assetId, amount, from, to, memo) if (!flags.confirm) { const confirmed = await CliUx.ux.confirm('Do you confirm (Y/N)?') diff --git a/ironfish-cli/src/utils/fees.ts b/ironfish-cli/src/utils/fees.ts index 4e0cdb86de..d281e82b3f 100644 --- a/ironfish-cli/src/utils/fees.ts +++ b/ironfish-cli/src/utils/fees.ts @@ -14,6 +14,7 @@ import { RpcClient, RpcRequestError, } from '@ironfish/sdk' +import { CliUx } from '@oclif/core' import inquirer from 'inquirer' import { promptCurrency } from './currency' @@ -24,26 +25,30 @@ export async function selectFee(options: { confirmations?: number logger: Logger }): Promise { + CliUx.ux.action.start('Calculating fees') + const feeRates = await options.client.wallet.estimateFeeRates() - const [slow, average, fast] = [ - await getTxWithFee( + const promises = [ + getTxWithFee( options.client, options.transaction, CurrencyUtils.decode(feeRates.content.slow), ), - await getTxWithFee( + getTxWithFee( options.client, options.transaction, CurrencyUtils.decode(feeRates.content.average), ), - await getTxWithFee( + getTxWithFee( options.client, options.transaction, CurrencyUtils.decode(feeRates.content.fast), ), ] + const [slow, average, fast] = await Promise.all(promises) + const choices = [ getChoiceFromTx('Slow', slow), getChoiceFromTx('Average', average), @@ -54,6 +59,8 @@ export async function selectFee(options: { }, ] + CliUx.ux.action.stop() + const result = await inquirer.prompt<{ selection: RawTransaction | null }>([ diff --git a/ironfish-cli/src/utils/transaction.ts b/ironfish-cli/src/utils/transaction.ts index 03293c66f4..a335e7d27a 100644 --- a/ironfish-cli/src/utils/transaction.ts +++ b/ironfish-cli/src/utils/transaction.ts @@ -4,14 +4,44 @@ import { createRootLogger, + CurrencyUtils, Logger, PromiseUtils, + RawTransaction, RpcClient, TimeUtils, TransactionStatus, } from '@ironfish/sdk' import { CliUx } from '@oclif/core' +export function displayTransactionSummary( + transaction: RawTransaction, + assetId: string, + amount: bigint, + from: string, + to: string, + memo: string, + logger?: Logger, +): void { + logger = logger ?? createRootLogger() + + const amountString = CurrencyUtils.renderIron(amount, true, assetId) + const feeString = CurrencyUtils.renderIron(transaction.fee, true) + + const summary = `\ +\nTRANSACTION SUMMARY: +From ${from} +To ${to} +Amount ${amountString} +Fee ${feeString} +Memo ${memo} +Outputs ${transaction.outputs.length} +Spends ${transaction.spends.length} +Expiration ${transaction.expiration ? transaction.expiration.toString() : ''} +` + logger.log(summary) +} + export async function watchTransaction(options: { client: Pick hash: string diff --git a/ironfish/src/fileStores/internal.ts b/ironfish/src/fileStores/internal.ts index 3b128217e0..1ce7251f42 100644 --- a/ironfish/src/fileStores/internal.ts +++ b/ironfish/src/fileStores/internal.ts @@ -11,6 +11,8 @@ export type InternalOptions = { telemetryNodeId: string rpcAuthToken: string networkId: number + spendPostTime: number // in milliseconds + spendPostTimeAt: number // when the spend post time measurement was done } export const InternalOptionsDefaults: InternalOptions = { @@ -19,6 +21,8 @@ export const InternalOptionsDefaults: InternalOptions = { telemetryNodeId: '', rpcAuthToken: '', networkId: DEFAULT_NETWORK_ID, + spendPostTime: 0, + spendPostTimeAt: 0, } export class InternalStore extends KeyStore { From dcb34cb64fd9283a1b4dc8c5a553fed6fe4047f3 Mon Sep 17 00:00:00 2001 From: Rahul Patni Date: Mon, 18 Dec 2023 18:58:49 -0500 Subject: [PATCH 5/6] bumping versions release v 1.15 (#4491) --- ironfish-cli/package.json | 4 ++-- ironfish/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ironfish-cli/package.json b/ironfish-cli/package.json index e23c4a4e32..5e5d302f10 100644 --- a/ironfish-cli/package.json +++ b/ironfish-cli/package.json @@ -1,6 +1,6 @@ { "name": "ironfish", - "version": "1.14.0", + "version": "1.15.0", "description": "CLI for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", @@ -63,7 +63,7 @@ "@aws-sdk/client-secrets-manager": "3", "@aws-sdk/s3-request-presigner": "3", "@ironfish/rust-nodejs": "1.12.0", - "@ironfish/sdk": "1.14.0", + "@ironfish/sdk": "1.15.0", "@oclif/core": "1.23.1", "@oclif/plugin-help": "5.1.12", "@oclif/plugin-not-found": "2.3.1", diff --git a/ironfish/package.json b/ironfish/package.json index 2554d4c537..63b04e66c9 100644 --- a/ironfish/package.json +++ b/ironfish/package.json @@ -1,6 +1,6 @@ { "name": "@ironfish/sdk", - "version": "1.14.0", + "version": "1.15.0", "description": "SDK for running and interacting with an Iron Fish node", "author": "Iron Fish (https://ironfish.network)", "main": "build/src/index.js", From 2d27708e26ddadcf4250a95d4111a0c0d8f9ec1c Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Tue, 19 Dec 2023 11:27:16 -0500 Subject: [PATCH 6/6] Bump perf test and regenerate fixture node versions to 20 (#4493) --- .github/workflows/ci-regenerate-fixtures.yml | 2 +- .github/workflows/perf_test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-regenerate-fixtures.yml b/.github/workflows/ci-regenerate-fixtures.yml index 5ce5444ca6..94ccff899d 100644 --- a/.github/workflows/ci-regenerate-fixtures.yml +++ b/.github/workflows/ci-regenerate-fixtures.yml @@ -18,7 +18,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 cache: 'yarn' - name: Cache Rust diff --git a/.github/workflows/perf_test.yml b/.github/workflows/perf_test.yml index bf937dc1f3..6276ce2729 100644 --- a/.github/workflows/perf_test.yml +++ b/.github/workflows/perf_test.yml @@ -24,7 +24,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 cache: 'yarn' - name: Install packages