diff --git a/.github/workflows/pr-type-category.yml b/.github/workflows/pr-type-category.yml index 0ae6369c..c33fce94 100644 --- a/.github/workflows/pr-type-category.yml +++ b/.github/workflows/pr-type-category.yml @@ -17,8 +17,10 @@ jobs: labels: "New Feature, Enhancement, Bug-Fix, Not-Yet-Enabled, Skip-Release-Notes" - name: "Checking for PR Category in PR title. Should be like ': '." + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - if [[ ! "${{ github.event.pull_request.title }}" =~ ^.{2,}\:.{2,} ]]; then + if [[ ! "$PR_TITLE" =~ ^.{2,}\:.{2,} ]]; then echo "## PR Category is missing from PR title. Please add it like ': '." >> GITHUB_STEP_SUMMARY exit 1 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf232f3..777858fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# 2.1.0 + +## What's Changed + +Supports new devmode block timestamp offset endpoints. + +### Bugfixes +* bugfix: Fix wrong response error type. by @winder in https://github.com/algorand/go-algorand-sdk/pull/461 +* debug: Remove debug output. by @winder in https://github.com/algorand/go-algorand-sdk/pull/465 +* bug: Fix extractError parsing by @Eric-Warehime in https://github.com/algorand/go-algorand-sdk/pull/492 +### Enhancements +* enhancement: add genesis type by @shiqizng in https://github.com/algorand/go-algorand-sdk/pull/443 +* docs: Update README.md by @algochoi in https://github.com/algorand/go-algorand-sdk/pull/460 +* tests: Add disassembly test for Go SDK by @algochoi in https://github.com/algorand/go-algorand-sdk/pull/462 +* marshaling: Lenient Address and BlockHash go-codec unmarshalers. by @winder in https://github.com/algorand/go-algorand-sdk/pull/464 +* API: Add types for ledgercore.StateDelta. by @winder in https://github.com/algorand/go-algorand-sdk/pull/467 +* docs: Create runnable examples to be pulled into docs by @barnjamin in https://github.com/algorand/go-algorand-sdk/pull/480 +* Docs: Examples by @barnjamin in https://github.com/algorand/go-algorand-sdk/pull/491 +* api: Regenerate client interfaces for timestamp, ready, and simulate endpoints by @algochoi in https://github.com/algorand/go-algorand-sdk/pull/513 +* Performance: Add MakeClientWithTransport client override that allows the user to pass a custom http transport by @pbennett in https://github.com/algorand/go-algorand-sdk/pull/520 +* DevOps: Add CODEOWNERS to restrict workflow editing by @onetechnical in https://github.com/algorand/go-algorand-sdk/pull/524 +* Performance: Add custom http transport to MakeClientWithTransport by @algochoi in https://github.com/algorand/go-algorand-sdk/pull/523 +### Other +* Regenerate code with the latest specification file (c90fd645) by @github-actions in https://github.com/algorand/go-algorand-sdk/pull/522 + +## New Contributors +* @pbennett made their first contribution in https://github.com/algorand/go-algorand-sdk/pull/520 + +**Full Changelog**: https://github.com/algorand/go-algorand-sdk/compare/v2.0.0...v2.1.0 + # 2.0.0 ## What's Changed diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..aa26c82a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +.github/ @algorand/dev +.circleci/ @algorand/dev diff --git a/Makefile b/Makefile index 698e71bb..239a3a85 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,9 @@ docker-gosdk-run: docker ps -a docker run -it --network host go-sdk-testing:latest +smoke-test-examples: + cd "$(SRCPATH)/examples" && bash smoke_test.sh && cd - + docker-test: harness docker-gosdk-build docker-gosdk-run diff --git a/README.md b/README.md index 17ff328c..7ffd6765 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The Algorand golang SDK provides: # Documentation -Full documentation is available [on godoc](https://godoc.org/github.com/algorand/go-algorand-sdk). You can also self-host the documentation by running `godoc -http=:8099` and visiting `http://localhost:8099/pkg/github.com/algorand/go-algorand-sdk` in your web browser. +Full documentation is available [on pkg.go.dev](https://pkg.go.dev/github.com/algorand/go-algorand-sdk/v2). You can also self-host the documentation by running `godoc -http=:8099` and visiting `http://localhost:8099/pkg/github.com/algorand/go-algorand-sdk` in your web browser. Additional developer documentation and examples can be found on [developer.algorand.org](https://developer.algorand.org/docs/sdks/go/) @@ -21,7 +21,7 @@ In `client/`, the `kmd` packages provide HTTP clients for the Key Management Dae In `client/v2` the `algod` package contains a client for the Algorand protocol daemon HTTP API. You can use it to check the status of the blockchain, read a block, look at transactions, or submit a signed transaction. In `client/v2` the `indexer` package contains a client for the Algorand Indexer API. You can use it to query historical transactions or make queries about the current state of the chain. -`future` package contains Transaction building utility functions. +`transaction` package contains Transaction building utility functions. `types` contains the data structures you'll use when interacting with the network, including addresses, transactions, multisig signatures, etc. diff --git a/_examples/account.go b/_examples/account.go new file mode 100644 index 00000000..146d7c57 --- /dev/null +++ b/_examples/account.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/mnemonic" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + // example: ACCOUNT_GENERATE + account := crypto.GenerateAccount() + mn, err := mnemonic.FromPrivateKey(account.PrivateKey) + + if err != nil { + log.Fatalf("failed to generate account: %s", err) + } + + log.Printf("Address: %s\n", account.Address) + log.Printf("Mnemonic: %s\n", mn) + // example: ACCOUNT_GENERATE + + // example: ACCOUNT_RECOVER_MNEMONIC + k, err := mnemonic.ToPrivateKey(mn) + if err != nil { + log.Fatalf("failed to parse mnemonic: %s", err) + } + + recovered, err := crypto.AccountFromPrivateKey(k) + if err != nil { + log.Fatalf("failed to recover account from key: %s", err) + } + + log.Printf("%+v", recovered) + // example: ACCOUNT_RECOVER_MNEMONIC + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + rekeyAccount(getAlgodClient(), accts[0], accts[1]) +} + +func rekeyAccount(algodClient *algod.Client, acct crypto.Account, rekeyTarget crypto.Account) { + // example: ACCOUNT_REKEY + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + addr := acct.Address.String() + // here we create a payment transaction but rekey is valid + // on any transaction type + rktxn, err := transaction.MakePaymentTxn(addr, addr, 0, nil, "", sp) + if err != nil { + log.Fatalf("failed to creating transaction: %s\n", err) + } + // Set the rekey parameter + rktxn.RekeyTo = rekeyTarget.Address + + _, stxn, err := crypto.SignTransaction(acct.PrivateKey, rktxn) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + } + + txID, err := algodClient.SendRawTransaction(stxn).Do(context.Background()) + if err != nil { + fmt.Printf("failed to send transaction: %s\n", err) + return + } + + result, err := transaction.WaitForConfirmation(algodClient, txID, 4, context.Background()) + if err != nil { + fmt.Printf("Error waiting for confirmation on txID: %s\n", txID) + return + } + + fmt.Printf("Confirmed Transaction: %s in Round %d\n", txID, result.ConfirmedRound) + // example: ACCOUNT_REKEY +} diff --git a/_examples/application/approval.teal b/_examples/application/approval.teal new file mode 100644 index 00000000..eabde624 --- /dev/null +++ b/_examples/application/approval.teal @@ -0,0 +1,100 @@ +#pragma version 4 +// Handle each possible OnCompletion type. We don't have to worry about +// handling ClearState, because the ClearStateProgram will execute in that +// case, not the ApprovalProgram. +txn ApplicationID +int 0 +== +bnz handle_approve + +txn OnCompletion +int NoOp +== +bnz handle_noop + +txn OnCompletion +int OptIn +== +bnz handle_approve + +txn OnCompletion +int CloseOut +== +bnz handle_closeout + +txn OnCompletion +int UpdateApplication +== +bnz handle_updateapp + +txn OnCompletion +int DeleteApplication +== +bnz handle_deleteapp + +// Unexpected OnCompletion value. Should be unreachable. +err + +handle_noop: +// Handle NoOp + +// read global state +byte "counter" +dup +app_global_get + +// increment the value +int 1 ++ + +// store to scratch space +dup +store 0 + +// update global state +app_global_put + +// read local state for sender +int 0 +byte "counter" +app_local_get + +// increment the value +int 1 ++ +store 1 + +// update local state for sender +int 0 +byte "counter" +load 1 +app_local_put + +// load return value as approval +load 0 +return + + +handle_closeout: +// Handle CloseOut +//approval +int 1 +return + +handle_deleteapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_updateapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_approve: +int 1 +return diff --git a/_examples/application/approval_refactored.teal b/_examples/application/approval_refactored.teal new file mode 100644 index 00000000..1af5a7eb --- /dev/null +++ b/_examples/application/approval_refactored.teal @@ -0,0 +1,107 @@ +#pragma version 4 +// Handle each possible OnCompletion type. We don't have to worry about +// handling ClearState, because the ClearStateProgram will execute in that +// case, not the ApprovalProgram. + +txn ApplicationID +int 0 +== +bnz handle_approve + +txn OnCompletion +int NoOp +== +bnz handle_noop + +txn OnCompletion +int OptIn +== +bnz handle_approve + +txn OnCompletion +int CloseOut +== +bnz handle_closeout + +txn OnCompletion +int UpdateApplication +== +bnz handle_updateapp + +txn OnCompletion +int DeleteApplication +== +bnz handle_deleteapp + +// Unexpected OnCompletion value. Should be unreachable. +err + +handle_noop: +// Handle NoOp + +// read global state +byte "counter" +dup +app_global_get + +// increment the value +int 1 ++ + +// store to scratch space +dup +store 0 + +// update global state +app_global_put + +// read local state for sender +int 0 +byte "counter" +app_local_get + +// increment the value +int 1 ++ +store 1 + +// update local state for sender +// update "counter" +int 0 +byte "counter" +load 1 +app_local_put + +// update "timestamp" +int 0 +byte "timestamp" +txn ApplicationArgs 0 +app_local_put + +// load return value as approval +load 0 +return + +handle_closeout: +// Handle CloseOut +//approval +int 1 +return + +handle_deleteapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_updateapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_approve: +int 1 +return \ No newline at end of file diff --git a/_examples/application/clear.teal b/_examples/application/clear.teal new file mode 100644 index 00000000..d793651c --- /dev/null +++ b/_examples/application/clear.teal @@ -0,0 +1,3 @@ +#pragma version 4 +int 1 +return \ No newline at end of file diff --git a/_examples/apps.go b/_examples/apps.go new file mode 100644 index 00000000..f996a15a --- /dev/null +++ b/_examples/apps.go @@ -0,0 +1,446 @@ +package main + +import ( + "context" + "encoding/base64" + "io/ioutil" + "log" + "time" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatal("failed to get sandbox accounts: %s", err) + } + acct1 := accts[0] + appID := appCreate(algodClient, acct1) + appOptIn(algodClient, appID, acct1) + + // example: APP_READ_STATE + // grab global state and config of application + appInfo, err := algodClient.GetApplicationByID(appID).Do(context.Background()) + if err != nil { + log.Fatalf("failed to get app info: %s", err) + } + log.Printf("app info: %+v", appInfo) + + // grab local state for an app id for a single account + acctInfo, err := algodClient.AccountApplicationInformation( + acct1.Address.String(), appID, + ).Do(context.Background()) + if err != nil { + log.Fatalf("failed to get app info: %s", err) + } + log.Printf("app info: %+v", acctInfo) + // example: APP_READ_STATE + + appNoOp(algodClient, appID, acct1) + appUpdate(algodClient, appID, acct1) + appCall(algodClient, appID, acct1) + appCloseOut(algodClient, appID, acct1) + appDelete(algodClient, appID, acct1) + +} + +func appCreate(algodClient *algod.Client, creator crypto.Account) uint64 { + // example: APP_SCHEMA + // declare application state storage (immutable) + var ( + localInts uint64 = 1 + localBytes uint64 = 1 + globalInts uint64 = 1 + globalBytes uint64 = 0 + ) + + // define schema + globalSchema := types.StateSchema{NumUint: globalInts, NumByteSlice: globalBytes} + localSchema := types.StateSchema{NumUint: localInts, NumByteSlice: localBytes} + // example: APP_SCHEMA + + // example: APP_SOURCE + approvalTeal, err := ioutil.ReadFile("application/approval.teal") + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + clearTeal, err := ioutil.ReadFile("application/clear.teal") + if err != nil { + log.Fatalf("failed to read clear program: %s", err) + } + // example: APP_SOURCE + + // example: APP_COMPILE + approvalResult, err := algodClient.TealCompile(approvalTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + approvalBinary, err := base64.StdEncoding.DecodeString(approvalResult.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + + clearResult, err := algodClient.TealCompile(clearTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + clearBinary, err := base64.StdEncoding.DecodeString(clearResult.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + // example: APP_COMPILE + + // example: APP_CREATE + // Create application + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeApplicationCreateTx( + false, approvalBinary, clearBinary, globalSchema, localSchema, + nil, nil, nil, nil, sp, creator.Address, nil, + types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + appID := confirmedTxn.ApplicationIndex + log.Printf("Created app with id: %d", appID) + // example: APP_CREATE + return appID +} + +func appOptIn(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_OPTIN + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a new clawback transaction with the target of the user address and the recipient as the creator + // address, being sent from the address marked as `clawback` on the asset, in this case the same as creator + txn, err := transaction.MakeApplicationOptInTx( + appID, nil, nil, nil, nil, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("OptIn Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_OPTIN +} + +func appNoOp(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_NOOP + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + // Add an arg to our app call + appArgs = append(appArgs, []byte("arg0")) + + txn, err := transaction.MakeApplicationNoOpTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("NoOp Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_NOOP +} + +func appUpdate(algodClient *algod.Client, appID uint64, caller crypto.Account) { + approvalBinary := compileTeal(algodClient, "application/approval_refactored.teal") + clearBinary := compileTeal(algodClient, "application/clear.teal") + + // example: APP_UPDATE + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationUpdateTx( + appID, appArgs, accts, apps, assets, approvalBinary, clearBinary, + sp, caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Update Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_UPDATE +} + +func appCloseOut(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_CLOSEOUT + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationCloseOutTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Closeout Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_CLOSEOUT +} + +func appClearState(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_CLEAR + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationClearStateTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("ClearState Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_CLEAR +} + +func appCall(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_CALL + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + datetime := time.Now().Format("2006-01-02 at 15:04:05") + appArgs = append(appArgs, []byte(datetime)) + + txn, err := transaction.MakeApplicationNoOpTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("NoOp Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_CALL +} + +func appDelete(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_DELETE + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationDeleteTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Delete Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_DELETE +} diff --git a/_examples/asa.go b/_examples/asa.go new file mode 100644 index 00000000..e8e28d36 --- /dev/null +++ b/_examples/asa.go @@ -0,0 +1,337 @@ +package main + +import ( + "context" + "log" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + creator := accts[0] + user := accts[1] + + assetID := createAsset(algodClient, creator) + // example: ASSET_INFO + info, err := algodClient.GetAssetByID(assetID).Do(context.Background()) + if err != nil { + log.Fatalf("failed to get asset info: %s", err) + } + log.Printf("Asset info for %d: %+v", assetID, info) + // example: ASSET_INFO + + configureAsset(algodClient, assetID, creator) + optInAsset(algodClient, assetID, user) + xferAsset(algodClient, assetID, creator, user) + freezeAsset(algodClient, assetID, creator, user) + clawbackAsset(algodClient, assetID, creator, user) + deleteAsset(algodClient, assetID, creator) +} + +func createAsset(algodClient *algod.Client, creator crypto.Account) uint64 { + // example: ASSET_CREATE + // Configure parameters for asset creation + var ( + creatorAddr = creator.Address.String() + assetName = "Really Useful Gift" + unitName = "rug" + assetURL = "https://path/to/my/asset/details" + assetMetadataHash = "thisIsSomeLength32HashCommitment" + defaultFrozen = false + decimals = uint32(0) + totalIssuance = uint64(1000) + + manager = creatorAddr + reserve = creatorAddr + freeze = creatorAddr + clawback = creatorAddr + + note []byte + ) + + // Get network-related transaction parameters and assign + txParams, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Construct the transaction + txn, err := transaction.MakeAssetCreateTxn( + creatorAddr, note, txParams, totalIssuance, decimals, + defaultFrozen, manager, reserve, freeze, clawback, + unitName, assetName, assetURL, assetMetadataHash, + ) + + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Create Transaction: %s confirmed in Round %d with new asset id: %d\n", + txid, confirmedTxn.ConfirmedRound, confirmedTxn.AssetIndex) + // example: ASSET_CREATE + return confirmedTxn.AssetIndex +} + +func configureAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account) { + // example: ASSET_CONFIG + creatorAddr := creator.Address.String() + var ( + newManager = creatorAddr + newFreeze = creatorAddr + newClawback = creatorAddr + newReserve = "" + + strictAddrCheck = false + note []byte + ) + + // Get network-related transaction parameters and assign + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetConfigTxn(creatorAddr, note, sp, assetID, newManager, newReserve, newFreeze, newClawback, strictAddrCheck) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Asset Config Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_CONFIG +} + +func optInAsset(algodClient *algod.Client, assetID uint64, user crypto.Account) { + // example: ASSET_OPTIN + userAddr := user.Address.String() + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetAcceptanceTxn(userAddr, nil, sp, assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(user.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("OptIn Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_OPTIN +} + +func xferAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account, user crypto.Account) { + // example: ASSET_XFER + var ( + creatorAddr = creator.Address.String() + userAddr = user.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetTransferTxn(creatorAddr, userAddr, 1, nil, sp, "", assetID) + if err != nil { + log.Fatalf("failed to make asset txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Asset Transfer Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_XFER +} + +func freezeAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account, user crypto.Account) { + // example: ASSET_FREEZE + var ( + creatorAddr = creator.Address.String() + userAddr = user.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a freeze asset transaction with the target of the user address + // and the new freeze setting of `true` + txn, err := transaction.MakeAssetFreezeTxn(creatorAddr, nil, sp, assetID, userAddr, true) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Freeze Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_FREEZE +} + +func clawbackAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account, user crypto.Account) { + // example: ASSET_CLAWBACK + var ( + creatorAddr = creator.Address.String() + userAddr = user.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a new clawback transaction with the target of the user address and the recipient as the creator + // address, being sent from the address marked as `clawback` on the asset, in this case the same as creator + txn, err := transaction.MakeAssetRevocationTxn(creatorAddr, userAddr, 1, creatorAddr, nil, sp, assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Clawback Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_CLAWBACK +} + +func deleteAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account) { + // example: ASSET_DELETE + var ( + creatorAddr = creator.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a new clawback transaction with the target of the user address and the recipient as the creator + // address, being sent from the address marked as `clawback` on the asset, in this case the same as creator + txn, err := transaction.MakeAssetDestroyTxn(creatorAddr, nil, sp, assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Destroy Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_DELETE +} diff --git a/_examples/atc.go b/_examples/atc.go new file mode 100644 index 00000000..d4d2cac2 --- /dev/null +++ b/_examples/atc.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "encoding/json" + "io/ioutil" + "log" + + "github.com/algorand/go-algorand-sdk/v2/abi" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + + appID := deployApp(algodClient, acct1) + log.Printf("%d", appID) + + // example: ATC_CONTRACT_INIT + b, err := ioutil.ReadFile("calculator/contract.json") + if err != nil { + log.Fatalf("failed to read contract file: %s", err) + } + + contract := &abi.Contract{} + if err := json.Unmarshal(b, contract); err != nil { + log.Fatalf("failed to unmarshal contract: %s", err) + } + // example: ATC_CONTRACT_INIT + + // example: ATC_CREATE + // Create the atc we'll use to compose our transaction group + var atc = transaction.AtomicTransactionComposer{} + // example: ATC_CREATE + + // example: ATC_ADD_TRANSACTION + // Get suggested params and make a transaction as usual + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakePaymentTxn(acct1.Address.String(), acct1.Address.String(), 10000, nil, "", sp) + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + + // Construct a TransactionWithSigner and pass it to the atc + signer := transaction.BasicAccountTransactionSigner{Account: acct1} + atc.AddTransaction(transaction.TransactionWithSigner{Txn: txn, Signer: signer}) + // example: ATC_ADD_TRANSACTION + + // example: ATC_ADD_METHOD_CALL + // Grab the method from out contract object + addMethod, err := contract.GetMethodByName("add") + if err != nil { + log.Fatalf("failed to get add method: %s", err) + } + + // Set up method call params + mcp := transaction.AddMethodCallParams{ + AppID: appID, + Sender: acct1.Address, + SuggestedParams: sp, + OnComplete: types.NoOpOC, + Signer: signer, + Method: addMethod, + MethodArgs: []interface{}{1, 1}, + } + if err := atc.AddMethodCall(mcp); err != nil { + log.Fatalf("failed to add method call: %s", err) + } + // example: ATC_ADD_METHOD_CALL + + // example: ATC_RESULTS + result, err := atc.Execute(algodClient, context.Background(), 4) + if err != nil { + log.Fatalf("failed to get add method: %s", err) + } + + for _, r := range result.MethodResults { + log.Printf("%s => %v", r.Method.Name, r.ReturnValue) + } + // example: ATC_RESULTS + + // example: ATC_BOX_REF + boxName := "coolBoxName" + mcp = transaction.AddMethodCallParams{ + AppID: appID, + Sender: acct1.Address, + SuggestedParams: sp, + OnComplete: types.NoOpOC, + Signer: signer, + Method: addMethod, + MethodArgs: []interface{}{1, 1}, + // Here we're passing a box reference so our app + // can reference it during evaluation + BoxReferences: []types.AppBoxReference{ + {AppID: appID, Name: []byte(boxName)}, + }, + } + // ... + // example: ATC_BOX_REF + +} diff --git a/_examples/atomic_transfer.go b/_examples/atomic_transfer.go new file mode 100644 index 00000000..72c989da --- /dev/null +++ b/_examples/atomic_transfer.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + acct2 := accts[1] + + // example: ATOMIC_CREATE_TXNS + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + tx1, err := transaction.MakePaymentTxn(acct1.Address.String(), acct2.Address.String(), 100000, nil, "", sp) + if err != nil { + log.Fatalf("failed creating transaction: %s", err) + } + + // from account 2 to account 1 + tx2, err := transaction.MakePaymentTxn(acct2.Address.String(), acct1.Address.String(), 100000, nil, "", sp) + if err != nil { + log.Fatalf("failed creating transaction: %s", err) + } + // example: ATOMIC_CREATE_TXNS + + // example: ATOMIC_GROUP_TXNS + // compute group id and put it into each transaction + gid, err := crypto.ComputeGroupID([]types.Transaction{tx1, tx2}) + tx1.Group = gid + tx2.Group = gid + // example: ATOMIC_GROUP_TXNS + + // example: ATOMIC_GROUP_SIGN + _, stx1, err := crypto.SignTransaction(acct1.PrivateKey, tx1) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + return + } + _, stx2, err := crypto.SignTransaction(acct2.PrivateKey, tx2) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + } + // example: ATOMIC_GROUP_SIGN + + // example: ATOMIC_GROUP_ASSEMBLE + var signedGroup []byte + signedGroup = append(signedGroup, stx1...) + signedGroup = append(signedGroup, stx2...) + + // example: ATOMIC_GROUP_ASSEMBLE + + // example: ATOMIC_GROUP_SEND + pendingTxID, err := algodClient.SendRawTransaction(signedGroup).Do(context.Background()) + if err != nil { + fmt.Printf("failed to send transaction: %s\n", err) + return + } + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, pendingTxID, 4, context.Background()) + if err != nil { + fmt.Printf("Error waiting for confirmation on txID: %s\n", pendingTxID) + return + } + fmt.Printf("Confirmed Transaction: %s in Round %d\n", pendingTxID, confirmedTxn.ConfirmedRound) + // example: ATOMIC_GROUP_SEND + +} diff --git a/_examples/calculator/approval.teal b/_examples/calculator/approval.teal new file mode 100644 index 00000000..32acbb84 --- /dev/null +++ b/_examples/calculator/approval.teal @@ -0,0 +1,181 @@ +#pragma version 8 +intcblock 0 1 +bytecblock 0x151f7c75 +txn NumAppArgs +intc_0 // 0 +== +bnz main_l10 +txna ApplicationArgs 0 +pushbytes 0xfe6bdf69 // "add(uint64,uint64)uint64" +== +bnz main_l9 +txna ApplicationArgs 0 +pushbytes 0xe2f188c5 // "mul(uint64,uint64)uint64" +== +bnz main_l8 +txna ApplicationArgs 0 +pushbytes 0x78b488b7 // "sub(uint64,uint64)uint64" +== +bnz main_l7 +txna ApplicationArgs 0 +pushbytes 0x16e80f08 // "div(uint64,uint64)uint64" +== +bnz main_l6 +err +main_l6: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 9 +txna ApplicationArgs 2 +btoi +store 10 +load 9 +load 10 +callsub div_3 +store 11 +bytec_0 // 0x151f7c75 +load 11 +itob +concat +log +intc_1 // 1 +return +main_l7: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub sub_2 +store 8 +bytec_0 // 0x151f7c75 +load 8 +itob +concat +log +intc_1 // 1 +return +main_l8: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 3 +txna ApplicationArgs 2 +btoi +store 4 +load 3 +load 4 +callsub mul_1 +store 5 +bytec_0 // 0x151f7c75 +load 5 +itob +concat +log +intc_1 // 1 +return +main_l9: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 0 +txna ApplicationArgs 2 +btoi +store 1 +load 0 +load 1 +callsub add_0 +store 2 +bytec_0 // 0x151f7c75 +load 2 +itob +concat +log +intc_1 // 1 +return +main_l10: +txn OnCompletion +intc_0 // NoOp +== +bnz main_l12 +err +main_l12: +txn ApplicationID +intc_0 // 0 +== +assert +intc_1 // 1 +return + +// add +add_0: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 ++ +frame_bury 0 +retsub + +// mul +mul_1: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 +* +frame_bury 0 +retsub + +// sub +sub_2: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 +- +frame_bury 0 +retsub + +// div +div_3: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 +/ +frame_bury 0 +retsub \ No newline at end of file diff --git a/_examples/calculator/clear.teal b/_examples/calculator/clear.teal new file mode 100644 index 00000000..e741f0e5 --- /dev/null +++ b/_examples/calculator/clear.teal @@ -0,0 +1,3 @@ +#pragma version 8 +pushint 0 // 0 +return \ No newline at end of file diff --git a/_examples/calculator/contract.json b/_examples/calculator/contract.json new file mode 100644 index 00000000..4b23fa17 --- /dev/null +++ b/_examples/calculator/contract.json @@ -0,0 +1,74 @@ +{ + "name": "Calculator", + "methods": [ + { + "name": "add", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Add a and b, return the result" + }, + { + "name": "mul", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Multiply a and b, return the result" + }, + { + "name": "sub", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Subtract b from a, return the result" + }, + { + "name": "div", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Divide a by b, return the result" + } + ], + "networks": {} +} \ No newline at end of file diff --git a/_examples/codec.go b/_examples/codec.go new file mode 100644 index 00000000..b0c7159c --- /dev/null +++ b/_examples/codec.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/binary" + "log" + "os" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + + // example: CODEC_TRANSACTION_UNSIGNED + // Error handling omitted for brevity + sp, _ := algodClient.SuggestedParams().Do(context.Background()) + ptxn, _ := transaction.MakePaymentTxn( + acct1.Address.String(), acct1.Address.String(), 10000, nil, "", sp, + ) + + // Encode the txn as bytes, + // if sending over the wire (like to a frontend) it should also be b64 encoded + encodedTxn := msgpack.Encode(ptxn) + os.WriteFile("pay.txn", encodedTxn, 0655) + + var recoveredPayTxn = types.Transaction{} + + msgpack.Decode(encodedTxn, &recoveredPayTxn) + log.Printf("%+v", recoveredPayTxn) + // example: CODEC_TRANSACTION_UNSIGNED + + // example: CODEC_TRANSACTION_SIGNED + // Assuming we already have a pay transaction `ptxn` + + // Sign the transaction + _, signedTxn, err := crypto.SignTransaction(acct1.PrivateKey, ptxn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Save the signed transaction to file + os.WriteFile("pay.stxn", signedTxn, 0644) + + signedPayTxn := types.SignedTxn{} + err = msgpack.Decode(signedTxn, &signedPayTxn) + if err != nil { + log.Fatalf("failed to decode signed transaction: %s", err) + } + // example: CODEC_TRANSACTION_SIGNED + + // example: CODEC_ADDRESS + address := "4H5UNRBJ2Q6JENAXQ6HNTGKLKINP4J4VTQBEPK5F3I6RDICMZBPGNH6KD4" + pk, _ := types.DecodeAddress(address) + addr := pk.String() + // example: CODEC_ADDRESS + _ = addr + + // example: CODEC_BASE64 + encoded := "SGksIEknbSBkZWNvZGVkIGZyb20gYmFzZTY0" + decoded, _ := base64.StdEncoding.DecodeString(encoded) + reencoded := base64.StdEncoding.EncodeToString(decoded) + // example: CODEC_BASE64 + _ = reencoded + + // example: CODEC_UINT64 + val := 1337 + encodedInt := make([]byte, 8) + binary.BigEndian.PutUint64(encodedInt, uint64(val)) + + decodedInt := binary.BigEndian.Uint64(encodedInt) + // decodedInt == val + // example: CODEC_UINT64 + _ = decodedInt + + // example: CODEC_BLOCK + // example: CODEC_BLOCK +} diff --git a/_examples/debug.go b/_examples/debug.go new file mode 100644 index 00000000..07f506ef --- /dev/null +++ b/_examples/debug.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + + appID := deployApp(algodClient, acct1) + + // example: DEBUG_DRYRUN_DUMP + var ( + args [][]byte + accounts []string + apps []uint64 + assets []uint64 + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + appCallTxn, err := transaction.MakeApplicationNoOpTx( + appID, args, accounts, apps, assets, sp, acct1.Address, + nil, types.Digest{}, [32]byte{}, types.Address{}, + ) + if err != nil { + log.Fatalf("Failed to create app call txn: %+v", err) + } + + _, stxn, err := crypto.SignTransaction(acct1.PrivateKey, appCallTxn) + if err != nil { + log.Fatalf("Failed to sign app txn: %+v", err) + } + + signedAppCallTxn := types.SignedTxn{} + msgpack.Decode(stxn, &signedAppCallTxn) + + drr, err := transaction.CreateDryrun(algodClient, []types.SignedTxn{signedAppCallTxn}, nil, context.Background()) + if err != nil { + log.Fatalf("Failed to create dryrun: %+v", err) + } + + os.WriteFile("dryrun.msgp", msgpack.Encode(drr), 0666) + // example: DEBUG_DRYRUN_DUMP + + // example: DEBUG_DRYRUN_SUBMIT + // Create the dryrun request object + drReq, err := transaction.CreateDryrun(algodClient, []types.SignedTxn{signedAppCallTxn}, nil, context.Background()) + if err != nil { + log.Fatalf("Failed to create dryrun: %+v", err) + } + + // Pass dryrun request to algod server + dryrunResponse, err := algodClient.TealDryrun(drReq).Do(context.Background()) + if err != nil { + log.Fatalf("failed to dryrun request: %s", err) + } + + // Inspect the response to check result + for _, txn := range dryrunResponse.Txns { + log.Printf("%+v", txn.AppCallTrace) + } + // example: DEBUG_DRYRUN_SUBMIT +} diff --git a/_examples/indexer.go b/_examples/indexer.go new file mode 100644 index 00000000..238550f1 --- /dev/null +++ b/_examples/indexer.go @@ -0,0 +1,135 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/client/v2/indexer" +) + +func main() { + // example: CREATE_INDEXER_CLIENT + // Create a new indexer client, configured to connect to out local sandbox + var indexerAddress = "http://localhost:8980" + var indexerToken = strings.Repeat("a", 64) + indexerClient, err := indexer.MakeClient( + indexerAddress, + indexerToken, + ) + + // Or, if necessary, pass alternate headers + + var indexerHeader common.Header + indexerHeader.Key = "X-API-Key" + indexerHeader.Value = indexerToken + indexerClientWithHeaders, err := indexer.MakeClientWithHeaders( + indexerAddress, + indexerToken, + []*common.Header{&indexerHeader}, + ) + // example: CREATE_INDEXER_CLIENT + + // Suppress `indexerClientWithHeaders declared but not used` + _ = indexerClientWithHeaders + + if err != nil { + fmt.Printf("failed to make indexer client: %s\n", err) + return + } + + indexerHealth, err := indexerClient.HealthCheck().Do(context.Background()) + if err != nil { + fmt.Printf("Failed to get status: %s\n", err) + return + } + + fmt.Printf("Indexer Round: %d\n", indexerHealth.Round) + + // example: INDEXER_LOOKUP_ASSET + // query parameters + var assetId uint64 = 2044572 + var minBalance uint64 = 50 + + // Lookup accounts with minimum balance of asset + assetResult, err := indexerClient. + LookupAssetBalances(assetId). + CurrencyGreaterThan(minBalance). + Do(context.Background()) + + // Print the results + assetJson, err := json.MarshalIndent(assetResult, "", "\t") + fmt.Printf(string(assetJson) + "\n") + // example: INDEXER_LOOKUP_ASSET + + assetJson = nil + + // example: INDEXER_SEARCH_MIN_AMOUNT + // query parameters + var transactionMinAmount uint64 = 10 + + // Query + transactionResult, err := indexerClient. + SearchForTransactions(). + CurrencyGreaterThan(transactionMinAmount). + Do(context.Background()) + + // Print results + transactionJson, err := json.MarshalIndent(transactionResult, "", "\t") + fmt.Printf(string(transactionJson) + "\n") + // example: INDEXER_SEARCH_MIN_AMOUNT + + // example: INDEXER_PAGINATE_RESULTS + var nextToken = "" + var numTx = 1 + var numPages = 1 + var pagedMinAmount uint64 = 10 + var limit uint64 = 1 + + for numTx > 0 { + // Query + pagedResults, err := indexerClient. + SearchForTransactions(). + CurrencyGreaterThan(pagedMinAmount). + Limit(limit). + NextToken(nextToken). + Do(context.Background()) + if err != nil { + return + } + pagedTransactions := pagedResults.Transactions + numTx = len(pagedTransactions) + nextToken = pagedResults.NextToken + + if numTx > 0 { + // Print results + pagedJson, err := json.MarshalIndent(pagedTransactions, "", "\t") + if err != nil { + return + } + fmt.Printf(string(pagedJson) + "\n") + fmt.Println("End of page : ", numPages) + fmt.Println("Transaction printed : ", len(pagedTransactions)) + fmt.Println("Next Token : ", nextToken) + numPages++ + } + } + // example: INDEXER_PAGINATE_RESULTS + + // example: INDEXER_PREFIX_SEARCH + // Parameters + var notePrefix = "showing prefix" + + // Query + prefixResult, err := indexerClient. + SearchForTransactions(). + NotePrefix([]byte(notePrefix)). + Do(context.Background()) + + // Print results + prefixJson, err := json.MarshalIndent(prefixResult, "", "\t") + fmt.Printf(string(prefixJson) + "\n") + // example: INDEXER_PREFIX_SEARCH +} diff --git a/_examples/kmd.go b/_examples/kmd.go new file mode 100644 index 00000000..6fa724df --- /dev/null +++ b/_examples/kmd.go @@ -0,0 +1,229 @@ +package main + +import ( + "crypto/ed25519" + "fmt" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/kmd" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/mnemonic" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + var ( + exampleWalletID string + exampleWalletHandleToken string + genResponse kmd.GenerateKeyResponse + initResponse kmd.InitWalletHandleResponse + ) + + // example: KMD_CREATE_CLIENT + // Create a new kmd client, configured to connect to out local sandbox + var kmdAddress = "http://localhost:4002" + var kmdToken = strings.Repeat("a", 64) + kmdClient, err := kmd.MakeClient( + kmdAddress, + kmdToken, + ) + // example: KMD_CREATE_CLIENT + + if err != nil { + fmt.Printf("failed to make kmd client: %s\n", err) + return + } + + // example: KMD_CREATE_WALLET + // Create the example wallet, if it doesn't already exist + createResponse, err := kmdClient.CreateWallet( + "DemoWallet", + "password", + kmd.DefaultWalletDriver, + types.MasterDerivationKey{}, + ) + if err != nil { + fmt.Printf("error creating wallet: %s\n", err) + return + } + + // We need the wallet ID in order to get a wallet handle, so we can add accounts + exampleWalletID = createResponse.Wallet.ID + fmt.Printf("Created wallet '%s' with ID: %s\n", createResponse.Wallet.Name, exampleWalletID) + // example: KMD_CREATE_WALLET + + // example: KMD_CREATE_ACCOUNT + // Get a wallet handle. + initResponse, _ = kmdClient.InitWalletHandle( + exampleWalletID, + "password", + ) + exampleWalletHandleToken = initResponse.WalletHandleToken + + // Generate a new address from the wallet handle + genResponse, err = kmdClient.GenerateKey(exampleWalletHandleToken) + if err != nil { + fmt.Printf("Error generating key: %s\n", err) + return + } + accountAddress := genResponse.Address + fmt.Printf("New Account: %s\n", accountAddress) + // example: KMD_CREATE_ACCOUNT + + // example: KMD_EXPORT_ACCOUNT + // Extract the account sk + accountKeyResponse, err := kmdClient.ExportKey( + exampleWalletHandleToken, + "password", + accountAddress, + ) + accountKey := accountKeyResponse.PrivateKey + // Convert sk to mnemonic + mn, err := mnemonic.FromPrivateKey(accountKey) + if err != nil { + fmt.Printf("Error getting backup phrase: %s\n", err) + return + } + fmt.Printf("Account Mnemonic: %v ", mn) + // example: KMD_EXPORT_ACCOUNT + + // example: KMD_IMPORT_ACCOUNT + account := crypto.GenerateAccount() + fmt.Println("Account Address: ", account.Address) + mn, err = mnemonic.FromPrivateKey(account.PrivateKey) + if err != nil { + fmt.Printf("Error getting backup phrase: %s\n", err) + return + } + fmt.Printf("Account Mnemonic: %s\n", mn) + importedAccount, err := kmdClient.ImportKey( + exampleWalletHandleToken, + account.PrivateKey, + ) + fmt.Println("Account Successfully Imported: ", importedAccount.Address) + // example: KMD_IMPORT_ACCOUNT + + // Get the MDK for Recovery example + backupResponse, err := kmdClient.ExportMasterDerivationKey(exampleWalletHandleToken, "password") + if err != nil { + fmt.Printf("error exporting mdk: %s\n", err) + return + } + backupPhrase, _ := mnemonic.FromMasterDerivationKey(backupResponse.MasterDerivationKey) + fmt.Printf("Backup: %s\n", backupPhrase) + + // example: KMD_RECOVER_WALLET + keyBytes, err := mnemonic.ToKey(backupPhrase) + if err != nil { + fmt.Printf("failed to get key: %s\n", err) + return + } + + var mdk types.MasterDerivationKey + copy(mdk[:], keyBytes) + recoverResponse, err := kmdClient.CreateWallet( + "RecoveryWallet", + "password", + kmd.DefaultWalletDriver, + mdk, + ) + if err != nil { + fmt.Printf("error creating wallet: %s\n", err) + return + } + + // We need the wallet ID in order to get a wallet handle, so we can add accounts + exampleWalletID = recoverResponse.Wallet.ID + fmt.Printf("Created wallet '%s' with ID: %s\n", recoverResponse.Wallet.Name, exampleWalletID) + + // Get a wallet handle. The wallet handle is used for things like signing transactions + // and creating accounts. Wallet handles do expire, but they can be renewed + initResponse, err = kmdClient.InitWalletHandle( + exampleWalletID, + "password", + ) + if err != nil { + fmt.Printf("Error initializing wallet handle: %s\n", err) + return + } + + // Extract the wallet handle + exampleWalletHandleToken = initResponse.WalletHandleToken + fmt.Printf("Got wallet handle: '%s'\n", exampleWalletHandleToken) + + // Generate a new address from the wallet handle + genResponse, err = kmdClient.GenerateKey(exampleWalletHandleToken) + if err != nil { + fmt.Printf("Error generating key: %s\n", err) + return + } + fmt.Printf("Recovered address %s\n", genResponse.Address) + // example: KMD_RECOVER_WALLET + + // example: ACCOUNT_GENERATE + newAccount := crypto.GenerateAccount() + passphrase, err := mnemonic.FromPrivateKey(newAccount.PrivateKey) + + if err != nil { + fmt.Printf("Error creating transaction: %s\n", err) + } else { + fmt.Printf("My address: %s\n", newAccount.Address) + fmt.Printf("My passphrase: %s\n", passphrase) + } + // example: ACCOUNT_GENERATE + + // example: MULTISIG_CREATE + // Get pre-defined set of keys for example + _, pks := loadAccounts() + addr1, _ := types.DecodeAddress(pks[1]) + addr2, _ := types.DecodeAddress(pks[2]) + addr3, _ := types.DecodeAddress(pks[3]) + + ma, err := crypto.MultisigAccountWithParams(1, 2, []types.Address{ + addr1, + addr2, + addr3, + }) + + if err != nil { + panic("invalid multisig parameters") + } + fromAddr, _ := ma.Address() + // Print multisig account + fmt.Printf("Multisig address : %s \n", fromAddr) + // example: MULTISIG_CREATE +} + +// Accounts to be used through examples +func loadAccounts() (map[int][]byte, map[int]string) { + // Shown for demonstration purposes. NEVER reveal secret mnemonics in practice. + // Change these values to use the accounts created previously. + // Paste in mnemonic phrases for all three accounts + acc1 := crypto.GenerateAccount() + mnemonic1, _ := mnemonic.FromPrivateKey(acc1.PrivateKey) + acc2 := crypto.GenerateAccount() + mnemonic2, _ := mnemonic.FromPrivateKey(acc2.PrivateKey) + acc3 := crypto.GenerateAccount() + mnemonic3, _ := mnemonic.FromPrivateKey(acc3.PrivateKey) + + mnemonics := []string{mnemonic1, mnemonic2, mnemonic3} + pks := map[int]string{1: "", 2: "", 3: ""} + var sks = make(map[int][]byte) + + for i, m := range mnemonics { + var err error + sk, err := mnemonic.ToPrivateKey(m) + sks[i+1] = sk + if err != nil { + fmt.Printf("Issue with account %d private key conversion.", i+1) + } + // derive public address from Secret Key. + pk := sk.Public() + var a types.Address + cpk := pk.(ed25519.PublicKey) + copy(a[:], cpk[:]) + pks[i+1] = a.String() + fmt.Printf("Loaded Key %d: %s\n", i+1, pks[i+1]) + } + return sks, pks +} diff --git a/_examples/lsig.go b/_examples/lsig.go new file mode 100644 index 00000000..f6deb891 --- /dev/null +++ b/_examples/lsig.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/binary" + "io/ioutil" + "log" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := getAlgodClient() + accts, err := getSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + seedAcct := accts[0] + + // example: LSIG_COMPILE + teal, err := ioutil.ReadFile("lsig/simple.teal") + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + + result, err := algodClient.TealCompile(teal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + lsigBinary, err := base64.StdEncoding.DecodeString(result.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + // example: LSIG_COMPILE + + // example: LSIG_INIT + lsig := crypto.LogicSigAccount{ + Lsig: types.LogicSig{Logic: lsigBinary, Args: nil}, + } + // example: LSIG_INIT + _ = lsig + + // example: LSIG_PASS_ARGS + encodedArg := make([]byte, 8) + binary.BigEndian.PutUint64(encodedArg, 123) + + lsigWithArgs := crypto.LogicSigAccount{ + Lsig: types.LogicSig{Logic: lsigBinary, Args: [][]byte{encodedArg}}, + } + // example: LSIG_PASS_ARGS + _ = lsigWithArgs + + // seed lsig so the pay from the lsig works + lsa, err := lsig.Address() + if err != nil { + log.Fatalf("failed to get lsig address: %s", err) + } + seedAddr := seedAcct.Address.String() + sp, _ := algodClient.SuggestedParams().Do(context.Background()) + txn, _ := transaction.MakePaymentTxn(seedAddr, lsa.String(), 1000000, nil, "", sp) + txid, stx, _ := crypto.SignTransaction(seedAcct.PrivateKey, txn) + algodClient.SendRawTransaction(stx).Do(context.Background()) + transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + + // example: LSIG_SIGN_FULL + sp, err = algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + lsigAddr, err := lsig.Address() + if err != nil { + log.Fatalf("failed to get lsig address: %s", err) + } + ptxn, err := transaction.MakePaymentTxn(lsigAddr.String(), seedAddr, 10000, nil, "", sp) + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + txid, stxn, err := crypto.SignLogicSigAccountTransaction(lsig, ptxn) + if err != nil { + log.Fatalf("failed to sign transaction with lsig: %s", err) + } + _, err = algodClient.SendRawTransaction(stxn).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + payResult, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("failed while waiting for transaction: %s", err) + } + log.Printf("Lsig pay confirmed in round: %d", payResult.ConfirmedRound) + // example: LSIG_SIGN_FULL + + // example: LSIG_DELEGATE_FULL + // account signs the logic, and now the logic may be passed instead + // of a signature for a transaction + var args [][]byte + delSig, err := crypto.MakeLogicSigAccountDelegated(lsigBinary, args, seedAcct.PrivateKey) + if err != nil { + log.Fatalf("failed to make delegate lsig: %s", err) + } + + delSigPay, err := transaction.MakePaymentTxn(seedAddr, lsigAddr.String(), 10000, nil, "", sp) + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + + delTxId, delStxn, err := crypto.SignLogicSigAccountTransaction(delSig, delSigPay) + if err != nil { + log.Fatalf("failed to sign with delegate sig: %s", err) + } + + _, err = algodClient.SendRawTransaction(delStxn).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + delPayResult, err := transaction.WaitForConfirmation(algodClient, delTxId, 4, context.Background()) + if err != nil { + log.Fatalf("failed while waiting for transaction: %s", err) + } + + log.Printf("Delegated Lsig pay confirmed in round: %d", delPayResult.ConfirmedRound) + // example: LSIG_DELEGATE_FULL +} diff --git a/_examples/lsig/sample_arg.teal b/_examples/lsig/sample_arg.teal new file mode 100644 index 00000000..8f21008e --- /dev/null +++ b/_examples/lsig/sample_arg.teal @@ -0,0 +1,5 @@ +#pragma version 5 +arg_0 +btoi +int 123 +== \ No newline at end of file diff --git a/_examples/lsig/simple.teal b/_examples/lsig/simple.teal new file mode 100644 index 00000000..d6298656 --- /dev/null +++ b/_examples/lsig/simple.teal @@ -0,0 +1,3 @@ +#pragma version 5 +int 1 +return \ No newline at end of file diff --git a/_examples/overview.go b/_examples/overview.go new file mode 100644 index 00000000..c3b64bb0 --- /dev/null +++ b/_examples/overview.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + + algodClient := getAlgodClient() + + nodeStatus, err := algodClient.Status().Do(context.Background()) + if err != nil { + fmt.Printf("Failed to get status: %s\n", err) + return + } + + fmt.Printf("Last Round: %d\n", nodeStatus.LastRound) + + // example: SP_MIN_FEE + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Printf("failed to %s", err) + } + // example: SP_MIN_FEE + + // example: CONST_MIN_FEE + log.Printf("Min fee const: %d", transaction.MinTxnFee) + // example: CONST_MIN_FEE + + // example: TRANSACTION_FEE_OVERRIDE + // by using fee pooling and setting our fee to 2x min tx fee + // we can cover the fee for another transaction in the group + sp.Fee = 2 * transaction.MinTxnFee + sp.FlatFee = true + // ... + // example: TRANSACTION_FEE_OVERRIDE + +} +func exampleAlgod() { + // example: ALGOD_CREATE_CLIENT + // Create a new algod client, configured to connect to out local sandbox + var algodAddress = "http://localhost:4001" + var algodToken = strings.Repeat("a", 64) + algodClient, _ := algod.MakeClient( + algodAddress, + algodToken, + ) + + // Or, if necessary, pass alternate headers + + var algodHeader common.Header + algodHeader.Key = "X-API-Key" + algodHeader.Value = algodToken + algodClientWithHeaders, _ := algod.MakeClientWithHeaders( + algodAddress, + algodToken, + []*common.Header{&algodHeader}, + ) + // example: ALGOD_CREATE_CLIENT + + _ = algodClientWithHeaders + _ = algodClient +} diff --git a/_examples/participation.go b/_examples/participation.go new file mode 100644 index 00000000..0c5afc10 --- /dev/null +++ b/_examples/participation.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + markOnline() +} + +func setupConnection() (c *algod.Client, err error) { + algod_token := strings.Repeat("a", 64) + algod_server := "http://127.0.0.1:4001" + algod_client, err := algod.MakeClient(algod_server, algod_token) + c = (*algod.Client)(algod_client) + return +} + +func markOnline() { + // setup connection + algodClient, err := setupConnection() + if err != nil { + fmt.Printf("error getting suggested tx params: %s\n", err) + return + } + + // get network suggested parameters + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + fmt.Printf("error getting suggested tx params: %s\n", err) + return + } + + // Mark Account as "Online" (participating) + // example: TRANSACTION_KEYREG_ONLINE_CREATE + fromAddr := "MWAPNXBDFFD2V5KWXAHWKBO7FO4JN36VR4CIBDKDDE7WAUAGZIXM3QPJW4" + voteKey := "87iBW46PP4BpTDz6+IEGvxY6JqEaOtV0g+VWcJqoqtc=" + selKey := "1V2BE2lbFvS937H7pJebN0zxkqe1Nrv+aVHDTPbYRlw=" + sProofKey := "f0CYOA4yXovNBFMFX+1I/tYVBaAl7VN6e0Ki5yZA3H6jGqsU/LYHNaBkMQ/rN4M4F3UmNcpaTmbVbq+GgDsrhQ==" + voteFirst := uint64(16532750) + voteLast := uint64(19532750) + keyDilution := uint64(1732) + nonpart := false + tx, err := transaction.MakeKeyRegTxnWithStateProofKey( + fromAddr, + []byte{}, + sp, + voteKey, + selKey, + sProofKey, + voteFirst, + voteLast, + keyDilution, + nonpart, + ) + // example: TRANSACTION_KEYREG_ONLINE_CREATE + if err != nil { + fmt.Printf("Error creating transaction: %s\n", err) + return + } + _ = tx +} diff --git a/_examples/util.go b/_examples/util.go new file mode 100644 index 00000000..0c9e40b8 --- /dev/null +++ b/_examples/util.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/kmd" + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +// add sandbox and other stuff +var ( + ALGOD_ADDRESS = "http://localhost:4001" + ALGOD_TOKEN = strings.Repeat("a", 64) + + KMD_ADDRESS = "http://localhost:4002" + KMD_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + KMD_WALLET_NAME = "unencrypted-default-wallet" + KMD_WALLET_PASSWORD = "" +) + +func getAlgodClient() *algod.Client { + algodClient, err := algod.MakeClient( + ALGOD_ADDRESS, + ALGOD_TOKEN, + ) + + if err != nil { + log.Fatalf("Failed to create algod client: %s", err) + } + + return algodClient +} + +func getSandboxAccounts() ([]crypto.Account, error) { + client, err := kmd.MakeClient(KMD_ADDRESS, KMD_TOKEN) + if err != nil { + return nil, fmt.Errorf("Failed to create client: %+v", err) + } + + resp, err := client.ListWallets() + if err != nil { + return nil, fmt.Errorf("Failed to list wallets: %+v", err) + } + + var walletId string + for _, wallet := range resp.Wallets { + if wallet.Name == KMD_WALLET_NAME { + walletId = wallet.ID + } + } + + if walletId == "" { + return nil, fmt.Errorf("No wallet named %s", KMD_WALLET_NAME) + } + + whResp, err := client.InitWalletHandle(walletId, KMD_WALLET_PASSWORD) + if err != nil { + return nil, fmt.Errorf("Failed to init wallet handle: %+v", err) + } + + addrResp, err := client.ListKeys(whResp.WalletHandleToken) + if err != nil { + return nil, fmt.Errorf("Failed to list keys: %+v", err) + } + + var accts []crypto.Account + for _, addr := range addrResp.Addresses { + expResp, err := client.ExportKey(whResp.WalletHandleToken, KMD_WALLET_PASSWORD, addr) + if err != nil { + return nil, fmt.Errorf("Failed to export key: %+v", err) + } + + acct, err := crypto.AccountFromPrivateKey(expResp.PrivateKey) + if err != nil { + return nil, fmt.Errorf("Failed to create account from private key: %+v", err) + } + + accts = append(accts, acct) + } + + return accts, nil +} + +func compileTeal(algodClient *algod.Client, path string) []byte { + teal, err := ioutil.ReadFile(path) + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + + result, err := algodClient.TealCompile(teal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + bin, err := base64.StdEncoding.DecodeString(result.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + return bin +} + +func deployApp(algodClient *algod.Client, creator crypto.Account) uint64 { + + var ( + approvalBinary = make([]byte, 1000) + clearBinary = make([]byte, 1000) + ) + + // Compile approval program + approvalTeal, err := ioutil.ReadFile("calculator/approval.teal") + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + + approvalResult, err := algodClient.TealCompile(approvalTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + _, err = base64.StdEncoding.Decode(approvalBinary, []byte(approvalResult.Result)) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + + // Compile clear program + clearTeal, err := ioutil.ReadFile("calculator/clear.teal") + if err != nil { + log.Fatalf("failed to read clear program: %s", err) + } + + clearResult, err := algodClient.TealCompile(clearTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + _, err = base64.StdEncoding.Decode(clearBinary, []byte(clearResult.Result)) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + + // Create application + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeApplicationCreateTx( + false, approvalBinary, clearBinary, + types.StateSchema{}, types.StateSchema{}, + nil, nil, nil, nil, + sp, creator.Address, nil, + types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + return confirmedTxn.ApplicationIndex +} diff --git a/client/v2/algod/accountApplicationInformation.go b/client/v2/algod/accountApplicationInformation.go index 6988ee50..a6f93d5e 100644 --- a/client/v2/algod/accountApplicationInformation.go +++ b/client/v2/algod/accountApplicationInformation.go @@ -11,7 +11,8 @@ import ( // AccountApplicationInformationParams contains all of the query parameters for url serialization. type AccountApplicationInformationParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` } diff --git a/client/v2/algod/accountAssetInformation.go b/client/v2/algod/accountAssetInformation.go index 997c978b..2a28d78c 100644 --- a/client/v2/algod/accountAssetInformation.go +++ b/client/v2/algod/accountAssetInformation.go @@ -11,7 +11,8 @@ import ( // AccountAssetInformationParams contains all of the query parameters for url serialization. type AccountAssetInformationParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` } diff --git a/client/v2/algod/accountInformation.go b/client/v2/algod/accountInformation.go index f601bb76..4096cded 100644 --- a/client/v2/algod/accountInformation.go +++ b/client/v2/algod/accountInformation.go @@ -16,7 +16,8 @@ type AccountInformationParams struct { // `none`. Exclude string `url:"exclude,omitempty"` - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` } diff --git a/client/v2/algod/algod.go b/client/v2/algod/algod.go index 73cb5f30..00c27afb 100644 --- a/client/v2/algod/algod.go +++ b/client/v2/algod/algod.go @@ -2,6 +2,7 @@ package algod import ( "context" + "net/http" "github.com/algorand/go-algorand-sdk/v2/client/v2/common" "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" @@ -53,10 +54,22 @@ func MakeClientWithHeaders(address string, apiToken string, headers []*common.He return } +// MakeClientWithTransport is the factory for constructing a Client for a given endpoint with a +// custom HTTP Transport as well as optional additional user defined headers. +func MakeClientWithTransport(address string, apiToken string, headers []*common.Header, transport http.RoundTripper) (c *Client, err error) { + commonClientWithTransport, err := common.MakeClientWithTransport(address, authHeader, apiToken, headers, transport) + c = (*Client)(commonClientWithTransport) + return +} + func (c *Client) HealthCheck() *HealthCheck { return &HealthCheck{c: c} } +func (c *Client) GetReady() *GetReady { + return &GetReady{c: c} +} + func (c *Client) GetGenesis() *GetGenesis { return &GetGenesis{c: c} } @@ -109,6 +122,10 @@ func (c *Client) SendRawTransaction(rawtxn []byte) *SendRawTransaction { return &SendRawTransaction{c: c, rawtxn: rawtxn} } +func (c *Client) SimulateTransaction(request models.SimulateRequest) *SimulateTransaction { + return &SimulateTransaction{c: c, request: request} +} + func (c *Client) SuggestedParams() *SuggestedParams { return &SuggestedParams{c: c} } @@ -121,10 +138,6 @@ func (c *Client) PendingTransactionInformation(txid string) *PendingTransactionI return &PendingTransactionInformation{c: c, txid: txid} } -func (c *Client) GetLedgerStateDelta(round uint64) *GetLedgerStateDelta { - return &GetLedgerStateDelta{c: c, round: round} -} - func (c *Client) GetStateProof(round uint64) *GetStateProof { return &GetStateProof{c: c, round: round} } @@ -173,6 +186,14 @@ func (c *Client) TealDryrun(request models.DryrunRequest) *TealDryrun { return &TealDryrun{c: c, request: request} } +func (c *Client) GetBlockTimeStampOffset() *GetBlockTimeStampOffset { + return &GetBlockTimeStampOffset{c: c} +} + +func (c *Client) SetBlockTimeStampOffset(offset uint64) *SetBlockTimeStampOffset { + return &SetBlockTimeStampOffset{c: c, offset: offset} +} + func (c *Client) BlockRaw(round uint64) *BlockRaw { return &BlockRaw{c: c, round: round} } diff --git a/client/v2/algod/getApplicationBoxByName.go b/client/v2/algod/getApplicationBoxByName.go index 1058fd40..87c8ed8e 100644 --- a/client/v2/algod/getApplicationBoxByName.go +++ b/client/v2/algod/getApplicationBoxByName.go @@ -18,11 +18,11 @@ type GetApplicationBoxByNameParams struct { Name string `url:"name,omitempty"` } -// GetApplicationBoxByName given an application ID and box name, it returns the box -// name and value (each base64 encoded). Box names must be in the goal app call arg -// encoding form 'encoding:value'. For ints, use the form 'int:1234'. For raw -// bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. -// For addresses, use the form 'addr:XYZ...'. +// GetApplicationBoxByName given an application ID and box name, it returns the +// round, box name, and value (each base64 encoded). Box names must be in the goal +// app call arg encoding form 'encoding:value'. For ints, use the form 'int:1234'. +// For raw bytes, use the form 'b64:A=='. For printable strings, use the form +// 'str:hello'. For addresses, use the form 'addr:XYZ...'. type GetApplicationBoxByName struct { c *Client diff --git a/client/v2/algod/getBlock.go b/client/v2/algod/getBlock.go index 9ae92460..40f0f1f5 100644 --- a/client/v2/algod/getBlock.go +++ b/client/v2/algod/getBlock.go @@ -12,7 +12,8 @@ import ( // BlockParams contains all of the query parameters for url serialization. type BlockParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` } diff --git a/client/v2/algod/getBlockTimeStampOffset.go b/client/v2/algod/getBlockTimeStampOffset.go new file mode 100644 index 00000000..34fd37f8 --- /dev/null +++ b/client/v2/algod/getBlockTimeStampOffset.go @@ -0,0 +1,19 @@ +package algod + +import ( + "context" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" +) + +// GetBlockTimeStampOffset gets the current timestamp offset. +type GetBlockTimeStampOffset struct { + c *Client +} + +// Do performs the HTTP request +func (s *GetBlockTimeStampOffset) Do(ctx context.Context, headers ...*common.Header) (response models.GetBlockTimeStampOffsetResponse, err error) { + err = s.c.get(ctx, &response, "/v2/devmode/blocks/offset", nil, headers) + return +} diff --git a/client/v2/algod/getLedgerStateDelta.go b/client/v2/algod/getLedgerStateDelta.go deleted file mode 100644 index f10040d6..00000000 --- a/client/v2/algod/getLedgerStateDelta.go +++ /dev/null @@ -1,22 +0,0 @@ -package algod - -import ( - "context" - "fmt" - - "github.com/algorand/go-algorand-sdk/v2/client/v2/common" - "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" -) - -// GetLedgerStateDelta get ledger deltas for a round. -type GetLedgerStateDelta struct { - c *Client - - round uint64 -} - -// Do performs the HTTP request -func (s *GetLedgerStateDelta) Do(ctx context.Context, headers ...*common.Header) (response models.LedgerStateDelta, err error) { - err = s.c.get(ctx, &response, fmt.Sprintf("/v2/deltas/%s", common.EscapeParams(s.round)...), nil, headers) - return -} diff --git a/client/v2/algod/getPendingTransactions.go b/client/v2/algod/getPendingTransactions.go index 12fb19e8..9b0e67b9 100644 --- a/client/v2/algod/getPendingTransactions.go +++ b/client/v2/algod/getPendingTransactions.go @@ -11,7 +11,8 @@ import ( // PendingTransactionsParams contains all of the query parameters for url serialization. type PendingTransactionsParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` // Max truncated number of transactions to display. If max=0, returns all pending diff --git a/client/v2/algod/getPendingTransactionsByAddress.go b/client/v2/algod/getPendingTransactionsByAddress.go index 8f2f80e2..fd714323 100644 --- a/client/v2/algod/getPendingTransactionsByAddress.go +++ b/client/v2/algod/getPendingTransactionsByAddress.go @@ -12,7 +12,8 @@ import ( // PendingTransactionsByAddressParams contains all of the query parameters for url serialization. type PendingTransactionsByAddressParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` // Max truncated number of transactions to display. If max=0, returns all pending diff --git a/client/v2/algod/getReady.go b/client/v2/algod/getReady.go new file mode 100644 index 00000000..998581a0 --- /dev/null +++ b/client/v2/algod/getReady.go @@ -0,0 +1,18 @@ +package algod + +import ( + "context" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" +) + +// GetReady returns OK if healthy and fully caught up. +type GetReady struct { + c *Client +} + +// Do performs the HTTP request +func (s *GetReady) Do(ctx context.Context, headers ...*common.Header) (response string, err error) { + err = s.c.get(ctx, &response, "/ready", nil, headers) + return +} diff --git a/client/v2/algod/getTransactionProof.go b/client/v2/algod/getTransactionProof.go index d3a5fad5..db65720e 100644 --- a/client/v2/algod/getTransactionProof.go +++ b/client/v2/algod/getTransactionProof.go @@ -11,7 +11,8 @@ import ( // GetTransactionProofParams contains all of the query parameters for url serialization. type GetTransactionProofParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` // Hashtype the type of hash function used to create the proof, must be one of: diff --git a/client/v2/algod/pendingTransactionInformation.go b/client/v2/algod/pendingTransactionInformation.go index c6212e45..671a4474 100644 --- a/client/v2/algod/pendingTransactionInformation.go +++ b/client/v2/algod/pendingTransactionInformation.go @@ -12,7 +12,8 @@ import ( // PendingTransactionInformationParams contains all of the query parameters for url serialization. type PendingTransactionInformationParams struct { - // Format configures whether the response object is JSON or MessagePack encoded. + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. Format string `url:"format,omitempty"` } diff --git a/client/v2/algod/rawTransaction.go b/client/v2/algod/rawTransaction.go index 1c8ed7a4..bbdc3435 100644 --- a/client/v2/algod/rawTransaction.go +++ b/client/v2/algod/rawTransaction.go @@ -8,7 +8,8 @@ import ( "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" ) -// SendRawTransaction broadcasts a raw transaction to the network. +// SendRawTransaction broadcasts a raw transaction or transaction group to the +// network. type SendRawTransaction struct { c *Client diff --git a/client/v2/algod/setBlockTimeStampOffset.go b/client/v2/algod/setBlockTimeStampOffset.go new file mode 100644 index 00000000..7487a472 --- /dev/null +++ b/client/v2/algod/setBlockTimeStampOffset.go @@ -0,0 +1,23 @@ +package algod + +import ( + "context" + "fmt" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" +) + +// SetBlockTimeStampOffset sets the timestamp offset (seconds) for blocks in dev +// mode. Providing an offset of 0 will unset this value and try to use the real +// clock for the timestamp. +type SetBlockTimeStampOffset struct { + c *Client + + offset uint64 +} + +// Do performs the HTTP request +func (s *SetBlockTimeStampOffset) Do(ctx context.Context, headers ...*common.Header) (response string, err error) { + err = s.c.post(ctx, &response, fmt.Sprintf("/v2/devmode/blocks/offset/%s", common.EscapeParams(s.offset)...), nil, headers, nil) + return +} diff --git a/client/v2/algod/simulateTransaction.go b/client/v2/algod/simulateTransaction.go new file mode 100644 index 00000000..ef5fdae6 --- /dev/null +++ b/client/v2/algod/simulateTransaction.go @@ -0,0 +1,33 @@ +package algod + +import ( + "context" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/client/v2/common/models" +) + +// SimulateTransactionParams contains all of the query parameters for url serialization. +type SimulateTransactionParams struct { + + // Format configures whether the response object is JSON or MessagePack encoded. If + // not provided, defaults to JSON. + Format string `url:"format,omitempty"` +} + +// SimulateTransaction simulates a raw transaction or transaction group as it would +// be evaluated on the network. The simulation will use blockchain state from the +// latest committed round. +type SimulateTransaction struct { + c *Client + + request models.SimulateRequest + + p SimulateTransactionParams +} + +// Do performs the HTTP request +func (s *SimulateTransaction) Do(ctx context.Context, headers ...*common.Header) (response models.SimulateResponse, err error) { + err = s.c.post(ctx, &response, "/v2/transactions/simulate", s.p, headers, s.request) + return +} diff --git a/client/v2/common/common.go b/client/v2/common/common.go index 0851848b..538fc63a 100644 --- a/client/v2/common/common.go +++ b/client/v2/common/common.go @@ -3,7 +3,6 @@ package common import ( "bytes" "context" - "fmt" "io" "io/ioutil" @@ -35,6 +34,7 @@ type Client struct { apiHeader string apiToken string headers []*Header + transport http.RoundTripper } // MakeClient is the factory for constructing a Client for a given endpoint. @@ -64,6 +64,19 @@ func MakeClientWithHeaders(address string, apiHeader, apiToken string, headers [ return } +// MakeClientWithTransport is the factory for constructing a Client for a given endpoint with a +// custom HTTP Transport as well as optional additional user defined headers. +func MakeClientWithTransport(address string, apiHeader, apiToken string, headers []*Header, transport http.RoundTripper) (c *Client, err error) { + c, err = MakeClientWithHeaders(address, apiHeader, apiToken, headers) + if err != nil { + return + } + + c.transport = transport + + return +} + type BadRequest error type InvalidToken error type NotFound error @@ -73,7 +86,7 @@ type InternalError error // If so, it returns the error. // Otherwise, it returns nil. func extractError(code int, errorBuf []byte) error { - if code == 200 { + if code >= 200 && code < 300 { return nil } @@ -108,9 +121,11 @@ func (client *Client) submitFormRaw(ctx context.Context, path string, params int queryURL := client.serverURL queryURL.Path += path - var req *http.Request - var bodyReader io.Reader - var v url.Values + var ( + req *http.Request + bodyReader io.Reader + v url.Values + ) if params != nil { v, err = query.Values(params) @@ -148,7 +163,7 @@ func (client *Client) submitFormRaw(ctx context.Context, path string, params int req.Header.Add(header.Key, header.Value) } - httpClient := &http.Client{} + httpClient := &http.Client{Transport: client.transport} req = req.WithContext(ctx) resp, err = httpClient.Do(req) @@ -181,7 +196,7 @@ func (client *Client) submitForm(ctx context.Context, response interface{}, path // The caller wants a string if strResponse, ok := response.(*string); ok { *strResponse = string(bodyBytes) - return err + return responseErr } // Attempt to unmarshal a response regardless of whether or not there was an error. diff --git a/client/v2/common/common_test.go b/client/v2/common/common_test.go index 8fdbe6e8..375041dc 100644 --- a/client/v2/common/common_test.go +++ b/client/v2/common/common_test.go @@ -2,14 +2,37 @@ package common import ( "context" + "fmt" "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestExtractError(t *testing.T) { + testcases := []struct { + name string + code int + err error + }{ + {name: "400", code: 400, err: BadRequest(fmt.Errorf("HTTP 400: "))}, + {name: "401", code: 401, err: InvalidToken(fmt.Errorf("HTTP 401: "))}, + {name: "404", code: 404, err: NotFound(fmt.Errorf("HTTP 404: "))}, + {name: "500", code: 500, err: InternalError(fmt.Errorf("HTTP 500: "))}, + {name: "200", code: 200, err: nil}, + {name: "201", code: 201, err: nil}, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.err, extractError(tc.code, []byte{})) + }) + } +} + func TestClient_Verbs(t *testing.T) { path := "/some/path" @@ -59,3 +82,37 @@ func TestClient_Verbs(t *testing.T) { }) } } + +func TestClientWithTransport(t *testing.T) { + var receivedMethod string + var receivedPath string + var receivedHeaderValue string + path := "/some/path" + + const headerKey string = "hello" + const headerValue string = "world" + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedMethod = r.Method + receivedPath = r.URL.String() + receivedHeaderValue = r.Header.Get(headerKey) + })) + + var header []*Header = []*Header{{Key: headerKey, Value: headerValue}} + var customTransport http.RoundTripper = &http.Transport{ + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + c, err := MakeClientWithTransport(mockServer.URL, "API-Header", "ASDF", header, customTransport) + require.NoError(t, err) + + // Call the test function. + err = c.Get(context.Background(), nil, path, nil, nil) + assert.Equal(t, "GET", receivedMethod) + assert.Equal(t, path, receivedPath) + assert.Equal(t, headerValue, receivedHeaderValue) + assert.Equal(t, c.transport, customTransport) +} diff --git a/client/v2/common/models/account_balance_record.go b/client/v2/common/models/account_balance_record.go deleted file mode 100644 index dffe48bf..00000000 --- a/client/v2/common/models/account_balance_record.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -// AccountBalanceRecord account and its address -type AccountBalanceRecord struct { - // AccountData updated account data. - AccountData Account `json:"account-data"` - - // Address address of the updated account. - Address string `json:"address"` -} diff --git a/client/v2/common/models/account_deltas.go b/client/v2/common/models/account_deltas.go deleted file mode 100644 index d23a4dcc..00000000 --- a/client/v2/common/models/account_deltas.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -// AccountDeltas exposes deltas for account based resources in a single round -type AccountDeltas struct { - // Accounts array of Account updates for the round - Accounts []AccountBalanceRecord `json:"accounts,omitempty"` - - // Apps array of App updates for the round. - Apps []AppResourceRecord `json:"apps,omitempty"` - - // Assets array of Asset updates for the round. - Assets []AssetResourceRecord `json:"assets,omitempty"` -} diff --git a/client/v2/common/models/account_totals.go b/client/v2/common/models/account_totals.go deleted file mode 100644 index ab0afd74..00000000 --- a/client/v2/common/models/account_totals.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -// AccountTotals total Algos in the system grouped by account status -type AccountTotals struct { - // NotParticipating amount of stake in non-participating accounts - NotParticipating uint64 `json:"not-participating"` - - // Offline amount of stake in offline accounts - Offline uint64 `json:"offline"` - - // Online amount of stake in online accounts - Online uint64 `json:"online"` - - // RewardsLevel total number of algos received per reward unit since genesis - RewardsLevel uint64 `json:"rewards-level"` -} diff --git a/client/v2/common/models/app_resource_record.go b/client/v2/common/models/app_resource_record.go deleted file mode 100644 index f1212b33..00000000 --- a/client/v2/common/models/app_resource_record.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -// AppResourceRecord represents AppParams and AppLocalStateDelta in deltas -type AppResourceRecord struct { - // Address app account address - Address string `json:"address"` - - // AppDeleted whether the app was deleted - AppDeleted bool `json:"app-deleted"` - - // AppIndex app index - AppIndex uint64 `json:"app-index"` - - // AppLocalState app local state - AppLocalState ApplicationLocalState `json:"app-local-state,omitempty"` - - // AppLocalStateDeleted whether the app local state was deleted - AppLocalStateDeleted bool `json:"app-local-state-deleted"` - - // AppParams app params - AppParams ApplicationParams `json:"app-params,omitempty"` -} diff --git a/client/v2/common/models/asset_resource_record.go b/client/v2/common/models/asset_resource_record.go deleted file mode 100644 index fcf008e2..00000000 --- a/client/v2/common/models/asset_resource_record.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -// AssetResourceRecord represents AssetParams and AssetHolding in deltas -type AssetResourceRecord struct { - // Address account address of the asset - Address string `json:"address"` - - // AssetDeleted whether the asset was deleted - AssetDeleted bool `json:"asset-deleted"` - - // AssetHolding the asset holding - AssetHolding AssetHolding `json:"asset-holding,omitempty"` - - // AssetHoldingDeleted whether the asset holding was deleted - AssetHoldingDeleted bool `json:"asset-holding-deleted"` - - // AssetIndex index of the asset - AssetIndex uint64 `json:"asset-index"` - - // AssetParams asset params - AssetParams AssetParams `json:"asset-params,omitempty"` -} diff --git a/client/v2/common/models/get_block_time_stamp_offset_response.go b/client/v2/common/models/get_block_time_stamp_offset_response.go new file mode 100644 index 00000000..9cb3b9df --- /dev/null +++ b/client/v2/common/models/get_block_time_stamp_offset_response.go @@ -0,0 +1,8 @@ +package models + +// GetBlockTimeStampOffsetResponse response containing the timestamp offset in +// seconds +type GetBlockTimeStampOffsetResponse struct { + // Offset timestamp offset in seconds. + Offset uint64 `json:"offset"` +} diff --git a/client/v2/common/models/ledger_state_delta.go b/client/v2/common/models/ledger_state_delta.go deleted file mode 100644 index d63dc849..00000000 --- a/client/v2/common/models/ledger_state_delta.go +++ /dev/null @@ -1,28 +0,0 @@ -package models - -// LedgerStateDelta contains ledger updates. -type LedgerStateDelta struct { - // Accts accountDeltas object - Accts AccountDeltas `json:"accts,omitempty"` - - // KvMods array of KV Deltas - KvMods []KvDelta `json:"kv-mods,omitempty"` - - // ModifiedApps list of modified Apps - ModifiedApps []ModifiedApp `json:"modified-apps,omitempty"` - - // ModifiedAssets list of modified Assets - ModifiedAssets []ModifiedAsset `json:"modified-assets,omitempty"` - - // PrevTimestamp previous block timestamp - PrevTimestamp uint64 `json:"prev-timestamp,omitempty"` - - // StateProofNext next round for which we expect a state proof - StateProofNext uint64 `json:"state-proof-next,omitempty"` - - // Totals account Totals - Totals AccountTotals `json:"totals,omitempty"` - - // TxLeases list of transaction leases - TxLeases []TxLease `json:"tx-leases,omitempty"` -} diff --git a/client/v2/common/models/modified_app.go b/client/v2/common/models/modified_app.go deleted file mode 100644 index bc58ff62..00000000 --- a/client/v2/common/models/modified_app.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -// ModifiedApp app which was created or deleted. -type ModifiedApp struct { - // Created created if true, deleted if false - Created bool `json:"created"` - - // Creator address of the creator. - Creator string `json:"creator"` - - // Id app Id - Id uint64 `json:"id"` -} diff --git a/client/v2/common/models/modified_asset.go b/client/v2/common/models/modified_asset.go deleted file mode 100644 index 684c848e..00000000 --- a/client/v2/common/models/modified_asset.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -// ModifiedAsset asset which was created or deleted. -type ModifiedAsset struct { - // Created created if true, deleted if false - Created bool `json:"created"` - - // Creator address of the creator. - Creator string `json:"creator"` - - // Id asset Id - Id uint64 `json:"id"` -} diff --git a/client/v2/common/models/node_status_response.go b/client/v2/common/models/node_status_response.go index 8f7f2b6d..b372fedc 100644 --- a/client/v2/common/models/node_status_response.go +++ b/client/v2/common/models/node_status_response.go @@ -66,4 +66,28 @@ type NodeStatusResponse struct { // TimeSinceLastRound timeSinceLastRound in nanoseconds TimeSinceLastRound uint64 `json:"time-since-last-round"` + + // UpgradeDelay upgrade delay + UpgradeDelay uint64 `json:"upgrade-delay,omitempty"` + + // UpgradeNextProtocolVoteBefore next protocol round + UpgradeNextProtocolVoteBefore uint64 `json:"upgrade-next-protocol-vote-before,omitempty"` + + // UpgradeNoVotes no votes cast for consensus upgrade + UpgradeNoVotes uint64 `json:"upgrade-no-votes,omitempty"` + + // UpgradeNodeVote this node's upgrade vote + UpgradeNodeVote bool `json:"upgrade-node-vote,omitempty"` + + // UpgradeVoteRounds total voting rounds for current upgrade + UpgradeVoteRounds uint64 `json:"upgrade-vote-rounds,omitempty"` + + // UpgradeVotes total votes cast for consensus upgrade + UpgradeVotes uint64 `json:"upgrade-votes,omitempty"` + + // UpgradeVotesRequired yes votes required for consensus upgrade + UpgradeVotesRequired uint64 `json:"upgrade-votes-required,omitempty"` + + // UpgradeYesVotes yes votes cast for consensus upgrade + UpgradeYesVotes uint64 `json:"upgrade-yes-votes,omitempty"` } diff --git a/client/v2/common/models/pending_transaction_response.go b/client/v2/common/models/pending_transaction_response.go index e8dd79e7..68bbe7b1 100644 --- a/client/v2/common/models/pending_transaction_response.go +++ b/client/v2/common/models/pending_transaction_response.go @@ -26,18 +26,18 @@ type PendingTransactionResponse struct { // ConfirmedRound the round where this transaction was confirmed, if present. ConfirmedRound uint64 `json:"confirmed-round,omitempty"` - // GlobalStateDelta (gd) Global state key/value changes for the application being + // GlobalStateDelta global state key/value changes for the application being // executed by this transaction. GlobalStateDelta []EvalDeltaKeyValue `json:"global-state-delta,omitempty"` // InnerTxns inner transactions produced by application execution. InnerTxns []PendingTransactionResponse `json:"inner-txns,omitempty"` - // LocalStateDelta (ld) Local state key/value changes for the application being - // executed by this transaction. + // LocalStateDelta local state key/value changes for the application being executed + // by this transaction. LocalStateDelta []AccountStateDelta `json:"local-state-delta,omitempty"` - // Logs (lg) Logs for the application being executed by this transaction. + // Logs logs for the application being executed by this transaction. Logs [][]byte `json:"logs,omitempty"` // PoolError indicates that the transaction was kicked out of this node's diff --git a/client/v2/common/models/simulate_request.go b/client/v2/common/models/simulate_request.go new file mode 100644 index 00000000..048ed202 --- /dev/null +++ b/client/v2/common/models/simulate_request.go @@ -0,0 +1,14 @@ +package models + +// SimulateRequest request type for simulation endpoint. +type SimulateRequest struct { + // AllowEmptySignatures allow transactions without signatures to be simulated as if + // they had correct signatures. + AllowEmptySignatures bool `json:"allow-empty-signatures,omitempty"` + + // AllowMoreLogging lifts limits on log opcode usage during simulation. + AllowMoreLogging bool `json:"allow-more-logging,omitempty"` + + // TxnGroups the transaction groups to simulate. + TxnGroups []SimulateRequestTransactionGroup `json:"txn-groups"` +} diff --git a/client/v2/common/models/simulate_request_transaction_group.go b/client/v2/common/models/simulate_request_transaction_group.go new file mode 100644 index 00000000..25126386 --- /dev/null +++ b/client/v2/common/models/simulate_request_transaction_group.go @@ -0,0 +1,9 @@ +package models + +import "github.com/algorand/go-algorand-sdk/v2/types" + +// SimulateRequestTransactionGroup a transaction group to simulate. +type SimulateRequestTransactionGroup struct { + // Txns an atomic transaction group. + Txns []types.SignedTxn `json:"txns"` +} diff --git a/client/v2/common/models/simulate_response.go b/client/v2/common/models/simulate_response.go new file mode 100644 index 00000000..51cdd10c --- /dev/null +++ b/client/v2/common/models/simulate_response.go @@ -0,0 +1,19 @@ +package models + +// SimulateResponse result of a transaction group simulation. +type SimulateResponse struct { + // EvalOverrides the set of parameters and limits override during simulation. If + // this set of parameters is present, then evaluation parameters may differ from + // standard evaluation in certain ways. + EvalOverrides SimulationEvalOverrides `json:"eval-overrides,omitempty"` + + // LastRound the round immediately preceding this simulation. State changes through + // this round were used to run this simulation. + LastRound uint64 `json:"last-round"` + + // TxnGroups a result object for each transaction group that was simulated. + TxnGroups []SimulateTransactionGroupResult `json:"txn-groups"` + + // Version the version of this response object. + Version uint64 `json:"version"` +} diff --git a/client/v2/common/models/simulate_transaction_group_result.go b/client/v2/common/models/simulate_transaction_group_result.go new file mode 100644 index 00000000..43429fc5 --- /dev/null +++ b/client/v2/common/models/simulate_transaction_group_result.go @@ -0,0 +1,25 @@ +package models + +// SimulateTransactionGroupResult simulation result for an atomic transaction group +type SimulateTransactionGroupResult struct { + // AppBudgetAdded total budget added during execution of app calls in the + // transaction group. + AppBudgetAdded uint64 `json:"app-budget-added,omitempty"` + + // AppBudgetConsumed total budget consumed during execution of app calls in the + // transaction group. + AppBudgetConsumed uint64 `json:"app-budget-consumed,omitempty"` + + // FailedAt if present, indicates which transaction in this group caused the + // failure. This array represents the path to the failing transaction. Indexes are + // zero based, the first element indicates the top-level transaction, and + // successive elements indicate deeper inner transactions. + FailedAt []uint64 `json:"failed-at,omitempty"` + + // FailureMessage if present, indicates that the transaction group failed and + // specifies why that happened + FailureMessage string `json:"failure-message,omitempty"` + + // TxnResults simulation result for individual transactions + TxnResults []SimulateTransactionResult `json:"txn-results"` +} diff --git a/client/v2/common/models/simulate_transaction_result.go b/client/v2/common/models/simulate_transaction_result.go new file mode 100644 index 00000000..3dab4b68 --- /dev/null +++ b/client/v2/common/models/simulate_transaction_result.go @@ -0,0 +1,15 @@ +package models + +// SimulateTransactionResult simulation result for an individual transaction +type SimulateTransactionResult struct { + // AppBudgetConsumed budget used during execution of an app call transaction. This + // value includes budged used by inner app calls spawned by this transaction. + AppBudgetConsumed uint64 `json:"app-budget-consumed,omitempty"` + + // LogicSigBudgetConsumed budget used during execution of a logic sig transaction. + LogicSigBudgetConsumed uint64 `json:"logic-sig-budget-consumed,omitempty"` + + // TxnResult details about a pending transaction. If the transaction was recently + // confirmed, includes confirmation details like the round and reward details. + TxnResult PendingTransactionResponse `json:"txn-result"` +} diff --git a/client/v2/common/models/simulation_eval_overrides.go b/client/v2/common/models/simulation_eval_overrides.go new file mode 100644 index 00000000..59510676 --- /dev/null +++ b/client/v2/common/models/simulation_eval_overrides.go @@ -0,0 +1,16 @@ +package models + +// SimulationEvalOverrides the set of parameters and limits override during +// simulation. If this set of parameters is present, then evaluation parameters may +// differ from standard evaluation in certain ways. +type SimulationEvalOverrides struct { + // AllowEmptySignatures if true, transactions without signatures are allowed and + // simulated as if they were properly signed. + AllowEmptySignatures bool `json:"allow-empty-signatures,omitempty"` + + // MaxLogCalls the maximum log calls one can make during simulation + MaxLogCalls uint64 `json:"max-log-calls,omitempty"` + + // MaxLogSize the maximum byte number to log during simulation + MaxLogSize uint64 `json:"max-log-size,omitempty"` +} diff --git a/client/v2/common/models/tx_lease.go b/client/v2/common/models/tx_lease.go deleted file mode 100644 index cdc25755..00000000 --- a/client/v2/common/models/tx_lease.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -// TxLease -type TxLease struct { - // Expiration round that the lease expires - Expiration uint64 `json:"expiration"` - - // Lease lease data - Lease []byte `json:"lease"` - - // Sender address of the lease sender - Sender string `json:"sender"` -} diff --git a/client/v2/indexer/indexer.go b/client/v2/indexer/indexer.go index a3d9f9fa..e0e5cf2c 100644 --- a/client/v2/indexer/indexer.go +++ b/client/v2/indexer/indexer.go @@ -2,6 +2,7 @@ package indexer import ( "context" + "net/http" "github.com/algorand/go-algorand-sdk/v2/client/v2/common" ) @@ -10,6 +11,11 @@ const authHeader = "X-Indexer-API-Token" type Client common.Client +// delete performs a DELETE request to the specific path against the server, assumes JSON response +func (c *Client) delete(ctx context.Context, response interface{}, path string, body interface{}, headers []*common.Header) error { + return (*common.Client)(c).Delete(ctx, response, path, body, headers) +} + // get performs a GET request to the specific path against the server, assumes JSON response func (c *Client) get(ctx context.Context, response interface{}, path string, body interface{}, headers []*common.Header) error { return (*common.Client)(c).Get(ctx, response, path, body, headers) @@ -47,6 +53,14 @@ func MakeClientWithHeaders(address string, apiToken string, headers []*common.He return } +// MakeClientWithTransport is the factory for constructing a Client for a given endpoint with a +// custom HTTP Transport as well as optional additional user defined headers. +func MakeClientWithTransport(address string, apiToken string, headers []*common.Header, transport http.RoundTripper) (c *Client, err error) { + commonClientWithTransport, err := common.MakeClientWithTransport(address, authHeader, apiToken, headers, transport) + c = (*Client)(commonClientWithTransport) + return +} + func (c *Client) HealthCheck() *HealthCheck { return &HealthCheck{c: c} } diff --git a/client/v2/indexer/lookupApplicationBoxByIDandName.go b/client/v2/indexer/lookupApplicationBoxByIDAndName.go similarity index 100% rename from client/v2/indexer/lookupApplicationBoxByIDandName.go rename to client/v2/indexer/lookupApplicationBoxByIDAndName.go diff --git a/encoding/json/json.go b/encoding/json/json.go index a150694e..3ec6fe5c 100644 --- a/encoding/json/json.go +++ b/encoding/json/json.go @@ -13,6 +13,10 @@ var CodecHandle *codec.JsonHandle // LenientCodecHandle is used to instantiate msgpack encoders for the REST API. var LenientCodecHandle *codec.JsonHandle +// JSONStrictHandle is the same as CodecHandle but with MapKeyAsString=true +// for correct maps[int]interface{} encoding +var JSONStrictHandle *codec.JsonHandle + // init configures our json encoder and decoder func init() { CodecHandle = new(codec.JsonHandle) @@ -30,6 +34,15 @@ func init() { LenientCodecHandle.RecursiveEmptyCheck = true LenientCodecHandle.Indent = 2 LenientCodecHandle.HTMLCharsAsIs = true + + JSONStrictHandle = new(codec.JsonHandle) + JSONStrictHandle.ErrorIfNoField = CodecHandle.ErrorIfNoField + JSONStrictHandle.ErrorIfNoArrayExpand = CodecHandle.ErrorIfNoArrayExpand + JSONStrictHandle.Canonical = CodecHandle.Canonical + JSONStrictHandle.RecursiveEmptyCheck = CodecHandle.RecursiveEmptyCheck + JSONStrictHandle.Indent = CodecHandle.Indent + JSONStrictHandle.HTMLCharsAsIs = CodecHandle.HTMLCharsAsIs + JSONStrictHandle.MapKeyAsString = true } // Encode returns a json-encoded byte buffer for a given object @@ -40,6 +53,15 @@ func Encode(obj interface{}) []byte { return b } +// EncodeStrict returns a JSON-encoded byte buffer for a given object +// It is the same Encode but encodes map's int keys as strings +func EncodeStrict(obj interface{}) []byte { + var b []byte + enc := codec.NewEncoderBytes(&b, JSONStrictHandle) + enc.MustEncode(obj) + return b +} + // Decode attempts to decode a json-encoded byte buffer into an // object instance pointed to by objptr func Decode(b []byte, objptr interface{}) error { diff --git a/encoding/json/json_test.go b/encoding/json/json_test.go index 8a677716..750408d4 100644 --- a/encoding/json/json_test.go +++ b/encoding/json/json_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type object struct { @@ -27,7 +28,7 @@ func TestDecode(t *testing.T) { // basic encode/decode test. var decoded object err := Decode(encodedOb, &decoded) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, obj, decoded) }) @@ -36,7 +37,7 @@ func TestDecode(t *testing.T) { decoder := NewDecoder(bytes.NewReader(encodedOb)) var decoded object err := decoder.Decode(&decoded) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, obj, decoded) }) @@ -45,7 +46,7 @@ func TestDecode(t *testing.T) { decoder := NewDecoder(bytes.NewReader(encodedOb)) var decoded subsetObject err := decoder.Decode(&decoded) - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "no matching struct field found when decoding stream map with key name") }) @@ -54,7 +55,32 @@ func TestDecode(t *testing.T) { decoder := NewLenientDecoder(bytes.NewReader(encodedOb)) var decoded subsetObject err := decoder.Decode(&decoded) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, obj.subsetObject, decoded) }) + + t.Run("original encode map key as string", func(t *testing.T) { + intMap := map[int]string{ + 0: "int key", + } + data := string(Encode(intMap)) + assert.NotContains(t, data, "\"0\":") + }) + + t.Run("strict encode map key as string", func(t *testing.T) { + intMap := map[int]string{ + 0: "int key", + } + data := string(EncodeStrict(intMap)) + assert.NotContains(t, data, "0:") + }) + + t.Run("strict encode map interface key as string", func(t *testing.T) { + t.Skip("There is a bug in go-codec with MapKeyAsString = true and Canonical = true") + intMap := map[interface{}]interface{}{ + 0: "int key", + } + data := string(EncodeStrict(intMap)) + assert.NotContains(t, data, "0:") + }) } diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..0d60aacb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,10 @@ +Algorand Go SDK Examples +----------------------- + +This directory contains examples of how to use the Algorand Go SDK. + +Assuming a sandbox node is running locally, any example can be run with the following command: + +```sh +go run /main.go +``` diff --git a/examples/account/main.go b/examples/account/main.go new file mode 100644 index 00000000..263a0cba --- /dev/null +++ b/examples/account/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/mnemonic" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + // example: ACCOUNT_GENERATE + account := crypto.GenerateAccount() + mn, err := mnemonic.FromPrivateKey(account.PrivateKey) + + if err != nil { + log.Fatalf("failed to generate account: %s", err) + } + + log.Printf("Address: %s\n", account.Address) + log.Printf("Mnemonic: %s\n", mn) + // example: ACCOUNT_GENERATE + + // example: ACCOUNT_RECOVER_MNEMONIC + k, err := mnemonic.ToPrivateKey(mn) + if err != nil { + log.Fatalf("failed to parse mnemonic: %s", err) + } + + recovered, err := crypto.AccountFromPrivateKey(k) + if err != nil { + log.Fatalf("failed to recover account from key: %s", err) + } + + log.Printf("%+v", recovered) + // example: ACCOUNT_RECOVER_MNEMONIC + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + rekeyAccount(examples.GetAlgodClient(), accts[0], accts[1]) +} + +func rekeyAccount(algodClient *algod.Client, acct crypto.Account, rekeyTarget crypto.Account) { + // example: ACCOUNT_REKEY + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + addr := acct.Address.String() + // here we create a payment transaction but rekey is valid + // on any transaction type + rktxn, err := transaction.MakePaymentTxn(addr, addr, 0, nil, "", sp) + if err != nil { + log.Fatalf("failed to creating transaction: %s\n", err) + } + // Set the rekey parameter + rktxn.RekeyTo = rekeyTarget.Address + + _, stxn, err := crypto.SignTransaction(acct.PrivateKey, rktxn) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + } + + txID, err := algodClient.SendRawTransaction(stxn).Do(context.Background()) + if err != nil { + fmt.Printf("failed to send transaction: %s\n", err) + return + } + + result, err := transaction.WaitForConfirmation(algodClient, txID, 4, context.Background()) + if err != nil { + fmt.Printf("Error waiting for confirmation on txID: %s\n", txID) + return + } + + fmt.Printf("Confirmed Transaction: %s in Round %d\n", txID, result.ConfirmedRound) + // example: ACCOUNT_REKEY + + // rekey back + rktxn, _ = transaction.MakePaymentTxn(addr, addr, 0, nil, "", sp) + rktxn.RekeyTo = acct.Address + _, stxn, _ = crypto.SignTransaction(rekeyTarget.PrivateKey, rktxn) + txID, _ = algodClient.SendRawTransaction(stxn).Do(context.Background()) + result, _ = transaction.WaitForConfirmation(algodClient, txID, 4, context.Background()) +} diff --git a/examples/application/approval.teal b/examples/application/approval.teal new file mode 100644 index 00000000..eabde624 --- /dev/null +++ b/examples/application/approval.teal @@ -0,0 +1,100 @@ +#pragma version 4 +// Handle each possible OnCompletion type. We don't have to worry about +// handling ClearState, because the ClearStateProgram will execute in that +// case, not the ApprovalProgram. +txn ApplicationID +int 0 +== +bnz handle_approve + +txn OnCompletion +int NoOp +== +bnz handle_noop + +txn OnCompletion +int OptIn +== +bnz handle_approve + +txn OnCompletion +int CloseOut +== +bnz handle_closeout + +txn OnCompletion +int UpdateApplication +== +bnz handle_updateapp + +txn OnCompletion +int DeleteApplication +== +bnz handle_deleteapp + +// Unexpected OnCompletion value. Should be unreachable. +err + +handle_noop: +// Handle NoOp + +// read global state +byte "counter" +dup +app_global_get + +// increment the value +int 1 ++ + +// store to scratch space +dup +store 0 + +// update global state +app_global_put + +// read local state for sender +int 0 +byte "counter" +app_local_get + +// increment the value +int 1 ++ +store 1 + +// update local state for sender +int 0 +byte "counter" +load 1 +app_local_put + +// load return value as approval +load 0 +return + + +handle_closeout: +// Handle CloseOut +//approval +int 1 +return + +handle_deleteapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_updateapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_approve: +int 1 +return diff --git a/examples/application/approval_refactored.teal b/examples/application/approval_refactored.teal new file mode 100644 index 00000000..1af5a7eb --- /dev/null +++ b/examples/application/approval_refactored.teal @@ -0,0 +1,107 @@ +#pragma version 4 +// Handle each possible OnCompletion type. We don't have to worry about +// handling ClearState, because the ClearStateProgram will execute in that +// case, not the ApprovalProgram. + +txn ApplicationID +int 0 +== +bnz handle_approve + +txn OnCompletion +int NoOp +== +bnz handle_noop + +txn OnCompletion +int OptIn +== +bnz handle_approve + +txn OnCompletion +int CloseOut +== +bnz handle_closeout + +txn OnCompletion +int UpdateApplication +== +bnz handle_updateapp + +txn OnCompletion +int DeleteApplication +== +bnz handle_deleteapp + +// Unexpected OnCompletion value. Should be unreachable. +err + +handle_noop: +// Handle NoOp + +// read global state +byte "counter" +dup +app_global_get + +// increment the value +int 1 ++ + +// store to scratch space +dup +store 0 + +// update global state +app_global_put + +// read local state for sender +int 0 +byte "counter" +app_local_get + +// increment the value +int 1 ++ +store 1 + +// update local state for sender +// update "counter" +int 0 +byte "counter" +load 1 +app_local_put + +// update "timestamp" +int 0 +byte "timestamp" +txn ApplicationArgs 0 +app_local_put + +// load return value as approval +load 0 +return + +handle_closeout: +// Handle CloseOut +//approval +int 1 +return + +handle_deleteapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_updateapp: +// Check for creator +global CreatorAddress +txn Sender +== +return + +handle_approve: +int 1 +return \ No newline at end of file diff --git a/examples/application/clear.teal b/examples/application/clear.teal new file mode 100644 index 00000000..d793651c --- /dev/null +++ b/examples/application/clear.teal @@ -0,0 +1,3 @@ +#pragma version 4 +int 1 +return \ No newline at end of file diff --git a/examples/apps/main.go b/examples/apps/main.go new file mode 100644 index 00000000..092c4d6f --- /dev/null +++ b/examples/apps/main.go @@ -0,0 +1,447 @@ +package main + +import ( + "context" + "encoding/base64" + "io/ioutil" + "log" + "time" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + acct1 := accts[0] + appID := appCreate(algodClient, acct1) + appOptIn(algodClient, appID, acct1) + + // example: APP_READ_STATE + // grab global state and config of application + appInfo, err := algodClient.GetApplicationByID(appID).Do(context.Background()) + if err != nil { + log.Fatalf("failed to get app info: %s", err) + } + log.Printf("app info: %+v", appInfo) + + // grab local state for an app id for a single account + acctInfo, err := algodClient.AccountApplicationInformation( + acct1.Address.String(), appID, + ).Do(context.Background()) + if err != nil { + log.Fatalf("failed to get app info: %s", err) + } + log.Printf("app info: %+v", acctInfo) + // example: APP_READ_STATE + + appNoOp(algodClient, appID, acct1) + appUpdate(algodClient, appID, acct1) + appCall(algodClient, appID, acct1) + appCloseOut(algodClient, appID, acct1) + appDelete(algodClient, appID, acct1) + +} + +func appCreate(algodClient *algod.Client, creator crypto.Account) uint64 { + // example: APP_SCHEMA + // declare application state storage (immutable) + var ( + localInts uint64 = 1 + localBytes uint64 = 1 + globalInts uint64 = 1 + globalBytes uint64 = 0 + ) + + // define schema + globalSchema := types.StateSchema{NumUint: globalInts, NumByteSlice: globalBytes} + localSchema := types.StateSchema{NumUint: localInts, NumByteSlice: localBytes} + // example: APP_SCHEMA + + // example: APP_SOURCE + approvalTeal, err := ioutil.ReadFile("application/approval.teal") + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + clearTeal, err := ioutil.ReadFile("application/clear.teal") + if err != nil { + log.Fatalf("failed to read clear program: %s", err) + } + // example: APP_SOURCE + + // example: APP_COMPILE + approvalResult, err := algodClient.TealCompile(approvalTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + approvalBinary, err := base64.StdEncoding.DecodeString(approvalResult.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + + clearResult, err := algodClient.TealCompile(clearTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + clearBinary, err := base64.StdEncoding.DecodeString(clearResult.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + // example: APP_COMPILE + + // example: APP_CREATE + // Create application + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeApplicationCreateTx( + false, approvalBinary, clearBinary, globalSchema, localSchema, + nil, nil, nil, nil, sp, creator.Address, nil, + types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + appID := confirmedTxn.ApplicationIndex + log.Printf("Created app with id: %d", appID) + // example: APP_CREATE + return appID +} + +func appOptIn(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_OPTIN + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a new clawback transaction with the target of the user address and the recipient as the creator + // address, being sent from the address marked as `clawback` on the asset, in this case the same as creator + txn, err := transaction.MakeApplicationOptInTx( + appID, nil, nil, nil, nil, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("OptIn Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_OPTIN +} + +func appNoOp(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_NOOP + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + // Add an arg to our app call + appArgs = append(appArgs, []byte("arg0")) + + txn, err := transaction.MakeApplicationNoOpTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("NoOp Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_NOOP +} + +func appUpdate(algodClient *algod.Client, appID uint64, caller crypto.Account) { + approvalBinary := examples.CompileTeal(algodClient, "application/approval_refactored.teal") + clearBinary := examples.CompileTeal(algodClient, "application/clear.teal") + + // example: APP_UPDATE + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationUpdateTx( + appID, appArgs, accts, apps, assets, approvalBinary, clearBinary, + sp, caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Update Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_UPDATE +} + +func appCloseOut(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_CLOSEOUT + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationCloseOutTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Closeout Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_CLOSEOUT +} + +func appClearState(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_CLEAR + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationClearStateTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("ClearState Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_CLEAR +} + +func appCall(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_CALL + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + datetime := time.Now().Format("2006-01-02 at 15:04:05") + appArgs = append(appArgs, []byte(datetime)) + + txn, err := transaction.MakeApplicationNoOpTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("NoOp Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_CALL +} + +func appDelete(algodClient *algod.Client, appID uint64, caller crypto.Account) { + // example: APP_DELETE + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + var ( + appArgs [][]byte + accts []string + apps []uint64 + assets []uint64 + ) + + txn, err := transaction.MakeApplicationDeleteTx( + appID, appArgs, accts, apps, assets, sp, + caller.Address, nil, types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(caller.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Delete Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: APP_DELETE +} diff --git a/examples/asa/main.go b/examples/asa/main.go new file mode 100644 index 00000000..2787a7a0 --- /dev/null +++ b/examples/asa/main.go @@ -0,0 +1,373 @@ +package main + +import ( + "context" + "log" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + creator := accts[0] + user := accts[1] + + assetID := createAsset(algodClient, creator) + // example: ASSET_INFO + info, err := algodClient.GetAssetByID(assetID).Do(context.Background()) + if err != nil { + log.Fatalf("failed to get asset info: %s", err) + } + log.Printf("Asset info for %d: %+v", assetID, info) + // example: ASSET_INFO + + configureAsset(algodClient, assetID, creator) + optInAsset(algodClient, assetID, user) + xferAsset(algodClient, assetID, creator, user) + freezeAsset(algodClient, assetID, creator, user) + clawbackAsset(algodClient, assetID, creator, user) + deleteAsset(algodClient, assetID, creator) +} + +func createAsset(algodClient *algod.Client, creator crypto.Account) uint64 { + // example: ASSET_CREATE + // Configure parameters for asset creation + var ( + creatorAddr = creator.Address.String() + assetName = "Really Useful Gift" + unitName = "rug" + assetURL = "https://path/to/my/asset/details" + assetMetadataHash = "thisIsSomeLength32HashCommitment" + defaultFrozen = false + decimals = uint32(0) + totalIssuance = uint64(1000) + + manager = creatorAddr + reserve = creatorAddr + freeze = creatorAddr + clawback = creatorAddr + + note []byte + ) + + // Get network-related transaction parameters and assign + txParams, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Construct the transaction + txn, err := transaction.MakeAssetCreateTxn( + creatorAddr, note, txParams, totalIssuance, decimals, + defaultFrozen, manager, reserve, freeze, clawback, + unitName, assetName, assetURL, assetMetadataHash, + ) + + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Create Transaction: %s confirmed in Round %d with new asset id: %d\n", + txid, confirmedTxn.ConfirmedRound, confirmedTxn.AssetIndex) + // example: ASSET_CREATE + return confirmedTxn.AssetIndex +} + +func configureAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account) { + // example: ASSET_CONFIG + creatorAddr := creator.Address.String() + var ( + newManager = creatorAddr + newFreeze = creatorAddr + newClawback = creatorAddr + newReserve = "" + + strictAddrCheck = false + note []byte + ) + + // Get network-related transaction parameters and assign + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetConfigTxn(creatorAddr, note, sp, assetID, newManager, newReserve, newFreeze, newClawback, strictAddrCheck) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Asset Config Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_CONFIG +} + +func optInAsset(algodClient *algod.Client, assetID uint64, user crypto.Account) { + // example: ASSET_OPTIN + userAddr := user.Address.String() + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetAcceptanceTxn(userAddr, nil, sp, assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(user.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("OptIn Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_OPTIN +} + +func optOutAsset(algodClient *algod.Client, assetID uint64, creator, user crypto.Account) { + // example: ASSET_OPT_OUT + userAddr := user.Address.String() + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetTransferTxn(userAddr, creator.Address.String(), 0, nil, sp, creator.Address.String(), assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(user.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("OptOut Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_OPT_OUT +} + +func xferAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account, user crypto.Account) { + // example: ASSET_XFER + var ( + creatorAddr = creator.Address.String() + userAddr = user.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeAssetTransferTxn(creatorAddr, userAddr, 1, nil, sp, "", assetID) + if err != nil { + log.Fatalf("failed to make asset txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Asset Transfer Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_XFER +} + +func freezeAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account, user crypto.Account) { + // example: ASSET_FREEZE + var ( + creatorAddr = creator.Address.String() + userAddr = user.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a freeze asset transaction with the target of the user address + // and the new freeze setting of `true` + txn, err := transaction.MakeAssetFreezeTxn(creatorAddr, nil, sp, assetID, userAddr, true) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Freeze Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_FREEZE +} + +func clawbackAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account, user crypto.Account) { + // example: ASSET_CLAWBACK + var ( + creatorAddr = creator.Address.String() + userAddr = user.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a new clawback transaction with the target of the user address and the recipient as the creator + // address, being sent from the address marked as `clawback` on the asset, in this case the same as creator + txn, err := transaction.MakeAssetRevocationTxn(creatorAddr, userAddr, 1, creatorAddr, nil, sp, assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Clawback Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_CLAWBACK +} + +func deleteAsset(algodClient *algod.Client, assetID uint64, creator crypto.Account) { + // example: ASSET_DELETE + var ( + creatorAddr = creator.Address.String() + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + // Create a new clawback transaction with the target of the user address and the recipient as the creator + // address, being sent from the address marked as `clawback` on the asset, in this case the same as creator + txn, err := transaction.MakeAssetDestroyTxn(creatorAddr, nil, sp, assetID) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + // sign the transaction + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Broadcast the transaction to the network + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + // Wait for confirmation + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + log.Printf("Destroy Transaction: %s confirmed in Round %d\n", txid, confirmedTxn.ConfirmedRound) + // example: ASSET_DELETE +} diff --git a/examples/atc/main.go b/examples/atc/main.go new file mode 100644 index 00000000..d0184ec0 --- /dev/null +++ b/examples/atc/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "encoding/json" + "io/ioutil" + "log" + + "github.com/algorand/go-algorand-sdk/v2/abi" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + + appID := examples.DeployApp(algodClient, acct1) + log.Printf("%d", appID) + + // example: ATC_CONTRACT_INIT + b, err := ioutil.ReadFile("calculator/contract.json") + if err != nil { + log.Fatalf("failed to read contract file: %s", err) + } + + contract := &abi.Contract{} + if err := json.Unmarshal(b, contract); err != nil { + log.Fatalf("failed to unmarshal contract: %s", err) + } + // example: ATC_CONTRACT_INIT + + // example: ATC_CREATE + // Create the atc we'll use to compose our transaction group + var atc = transaction.AtomicTransactionComposer{} + // example: ATC_CREATE + + // example: ATC_ADD_TRANSACTION + // Get suggested params and make a transaction as usual + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakePaymentTxn(acct1.Address.String(), acct1.Address.String(), 10000, nil, "", sp) + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + + // Construct a TransactionWithSigner and pass it to the atc + signer := transaction.BasicAccountTransactionSigner{Account: acct1} + atc.AddTransaction(transaction.TransactionWithSigner{Txn: txn, Signer: signer}) + // example: ATC_ADD_TRANSACTION + + // example: ATC_ADD_METHOD_CALL + // Grab the method from out contract object + addMethod, err := contract.GetMethodByName("add") + if err != nil { + log.Fatalf("failed to get add method: %s", err) + } + + // Set up method call params + mcp := transaction.AddMethodCallParams{ + AppID: appID, + Sender: acct1.Address, + SuggestedParams: sp, + OnComplete: types.NoOpOC, + Signer: signer, + Method: addMethod, + MethodArgs: []interface{}{1, 1}, + } + if err := atc.AddMethodCall(mcp); err != nil { + log.Fatalf("failed to add method call: %s", err) + } + // example: ATC_ADD_METHOD_CALL + + // example: ATC_RESULTS + result, err := atc.Execute(algodClient, context.Background(), 4) + if err != nil { + log.Fatalf("failed to get add method: %s", err) + } + + for _, r := range result.MethodResults { + log.Printf("%s => %v", r.Method.Name, r.ReturnValue) + } + // example: ATC_RESULTS + + // example: ATC_BOX_REF + boxName := "coolBoxName" + mcp = transaction.AddMethodCallParams{ + AppID: appID, + Sender: acct1.Address, + SuggestedParams: sp, + OnComplete: types.NoOpOC, + Signer: signer, + Method: addMethod, + MethodArgs: []interface{}{1, 1}, + // Here we're passing a box reference so our app + // can reference it during evaluation + BoxReferences: []types.AppBoxReference{ + {AppID: appID, Name: []byte(boxName)}, + }, + } + // ... + // example: ATC_BOX_REF + +} diff --git a/examples/atomic_transactions/main.go b/examples/atomic_transactions/main.go new file mode 100644 index 00000000..3d0f5815 --- /dev/null +++ b/examples/atomic_transactions/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + acct2 := accts[1] + + // example: ATOMIC_CREATE_TXNS + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + tx1, err := transaction.MakePaymentTxn(acct1.Address.String(), acct2.Address.String(), 100000, nil, "", sp) + if err != nil { + log.Fatalf("failed creating transaction: %s", err) + } + + // from account 2 to account 1 + tx2, err := transaction.MakePaymentTxn(acct2.Address.String(), acct1.Address.String(), 100000, nil, "", sp) + if err != nil { + log.Fatalf("failed creating transaction: %s", err) + } + // example: ATOMIC_CREATE_TXNS + + // example: ATOMIC_GROUP_TXNS + // compute group id and put it into each transaction + gid, _ := crypto.ComputeGroupID([]types.Transaction{tx1, tx2}) + tx1.Group = gid + tx2.Group = gid + // example: ATOMIC_GROUP_TXNS + + // example: ATOMIC_GROUP_SIGN + _, stx1, err := crypto.SignTransaction(acct1.PrivateKey, tx1) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + return + } + _, stx2, err := crypto.SignTransaction(acct2.PrivateKey, tx2) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + } + // example: ATOMIC_GROUP_SIGN + + // example: ATOMIC_GROUP_ASSEMBLE + var signedGroup []byte + signedGroup = append(signedGroup, stx1...) + signedGroup = append(signedGroup, stx2...) + + // example: ATOMIC_GROUP_ASSEMBLE + + // example: ATOMIC_GROUP_SEND + pendingTxID, err := algodClient.SendRawTransaction(signedGroup).Do(context.Background()) + if err != nil { + fmt.Printf("failed to send transaction: %s\n", err) + return + } + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, pendingTxID, 4, context.Background()) + if err != nil { + fmt.Printf("Error waiting for confirmation on txID: %s\n", pendingTxID) + return + } + fmt.Printf("Confirmed Transaction: %s in Round %d\n", pendingTxID, confirmedTxn.ConfirmedRound) + // example: ATOMIC_GROUP_SEND + +} diff --git a/examples/calculator/approval.teal b/examples/calculator/approval.teal new file mode 100644 index 00000000..32acbb84 --- /dev/null +++ b/examples/calculator/approval.teal @@ -0,0 +1,181 @@ +#pragma version 8 +intcblock 0 1 +bytecblock 0x151f7c75 +txn NumAppArgs +intc_0 // 0 +== +bnz main_l10 +txna ApplicationArgs 0 +pushbytes 0xfe6bdf69 // "add(uint64,uint64)uint64" +== +bnz main_l9 +txna ApplicationArgs 0 +pushbytes 0xe2f188c5 // "mul(uint64,uint64)uint64" +== +bnz main_l8 +txna ApplicationArgs 0 +pushbytes 0x78b488b7 // "sub(uint64,uint64)uint64" +== +bnz main_l7 +txna ApplicationArgs 0 +pushbytes 0x16e80f08 // "div(uint64,uint64)uint64" +== +bnz main_l6 +err +main_l6: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 9 +txna ApplicationArgs 2 +btoi +store 10 +load 9 +load 10 +callsub div_3 +store 11 +bytec_0 // 0x151f7c75 +load 11 +itob +concat +log +intc_1 // 1 +return +main_l7: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub sub_2 +store 8 +bytec_0 // 0x151f7c75 +load 8 +itob +concat +log +intc_1 // 1 +return +main_l8: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 3 +txna ApplicationArgs 2 +btoi +store 4 +load 3 +load 4 +callsub mul_1 +store 5 +bytec_0 // 0x151f7c75 +load 5 +itob +concat +log +intc_1 // 1 +return +main_l9: +txn OnCompletion +intc_0 // NoOp +== +txn ApplicationID +intc_0 // 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 0 +txna ApplicationArgs 2 +btoi +store 1 +load 0 +load 1 +callsub add_0 +store 2 +bytec_0 // 0x151f7c75 +load 2 +itob +concat +log +intc_1 // 1 +return +main_l10: +txn OnCompletion +intc_0 // NoOp +== +bnz main_l12 +err +main_l12: +txn ApplicationID +intc_0 // 0 +== +assert +intc_1 // 1 +return + +// add +add_0: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 ++ +frame_bury 0 +retsub + +// mul +mul_1: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 +* +frame_bury 0 +retsub + +// sub +sub_2: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 +- +frame_bury 0 +retsub + +// div +div_3: +proto 2 1 +intc_0 // 0 +frame_dig -2 +frame_dig -1 +/ +frame_bury 0 +retsub \ No newline at end of file diff --git a/examples/calculator/clear.teal b/examples/calculator/clear.teal new file mode 100644 index 00000000..e741f0e5 --- /dev/null +++ b/examples/calculator/clear.teal @@ -0,0 +1,3 @@ +#pragma version 8 +pushint 0 // 0 +return \ No newline at end of file diff --git a/examples/calculator/contract.json b/examples/calculator/contract.json new file mode 100644 index 00000000..4b23fa17 --- /dev/null +++ b/examples/calculator/contract.json @@ -0,0 +1,74 @@ +{ + "name": "Calculator", + "methods": [ + { + "name": "add", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Add a and b, return the result" + }, + { + "name": "mul", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Multiply a and b, return the result" + }, + { + "name": "sub", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Subtract b from a, return the result" + }, + { + "name": "div", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Divide a by b, return the result" + } + ], + "networks": {} +} \ No newline at end of file diff --git a/examples/codec/main.go b/examples/codec/main.go new file mode 100644 index 00000000..b896912b --- /dev/null +++ b/examples/codec/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/binary" + "log" + "os" + + "github.com/algorand/go-algorand-sdk/v2/abi" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + + // example: CODEC_TRANSACTION_UNSIGNED + // Error handling omitted for brevity + sp, _ := algodClient.SuggestedParams().Do(context.Background()) + ptxn, _ := transaction.MakePaymentTxn( + acct1.Address.String(), acct1.Address.String(), 10000, nil, "", sp, + ) + + // Encode the txn as bytes, + // if sending over the wire (like to a frontend) it should also be b64 encoded + encodedTxn := msgpack.Encode(ptxn) + os.WriteFile("pay.txn", encodedTxn, 0655) + + var recoveredPayTxn = types.Transaction{} + + msgpack.Decode(encodedTxn, &recoveredPayTxn) + log.Printf("%+v", recoveredPayTxn) + // example: CODEC_TRANSACTION_UNSIGNED + + // example: CODEC_TRANSACTION_SIGNED + // Assuming we already have a pay transaction `ptxn` + + // Sign the transaction + _, signedTxn, err := crypto.SignTransaction(acct1.PrivateKey, ptxn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + // Save the signed transaction to file + os.WriteFile("pay.stxn", signedTxn, 0644) + + signedPayTxn := types.SignedTxn{} + err = msgpack.Decode(signedTxn, &signedPayTxn) + if err != nil { + log.Fatalf("failed to decode signed transaction: %s", err) + } + // example: CODEC_TRANSACTION_SIGNED + + // cleanup + os.Remove("pay.stxn") + os.Remove("pay.txn") + + // example: CODEC_ADDRESS + address := "4H5UNRBJ2Q6JENAXQ6HNTGKLKINP4J4VTQBEPK5F3I6RDICMZBPGNH6KD4" + pk, _ := types.DecodeAddress(address) + addr := pk.String() + // example: CODEC_ADDRESS + _ = addr + + // example: CODEC_BASE64 + encoded := "SGksIEknbSBkZWNvZGVkIGZyb20gYmFzZTY0" + decoded, _ := base64.StdEncoding.DecodeString(encoded) + reencoded := base64.StdEncoding.EncodeToString(decoded) + // example: CODEC_BASE64 + _ = reencoded + + // example: CODEC_UINT64 + val := 1337 + encodedInt := make([]byte, 8) + binary.BigEndian.PutUint64(encodedInt, uint64(val)) + + decodedInt := binary.BigEndian.Uint64(encodedInt) + // decodedInt == val + // example: CODEC_UINT64 + _ = decodedInt + + // example: CODEC_ABI + tupleCodec, _ := abi.TypeOf("(string,string)") + + tupleVal := []string{"hello", "world"} + encodedTuple, _ := tupleCodec.Encode(tupleVal) + log.Printf("%x", encodedTuple) + + decodedTuple, _ := tupleCodec.Decode(encodedTuple) + log.Printf("%v", decodedTuple) // [hello world] + + arrCodec, _ := abi.TypeOf("uint64[]") + arrVal := []uint64{1, 2, 3, 4, 5} + encodedArr, _ := arrCodec.Encode(arrVal) + log.Printf("%x", encodedArr) + + decodedArr, _ := arrCodec.Decode(encodedArr) + log.Printf("%v", decodedArr) // [1 2 3 4 5] + // example: CODEC_ABI +} diff --git a/examples/debug/main.go b/examples/debug/main.go new file mode 100644 index 00000000..2d9fda18 --- /dev/null +++ b/examples/debug/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + + acct1 := accts[0] + + appID := examples.DeployApp(algodClient, acct1) + + // example: DEBUG_DRYRUN_DUMP + var ( + args [][]byte + accounts []string + apps []uint64 + assets []uint64 + ) + + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + appCallTxn, err := transaction.MakeApplicationNoOpTx( + appID, args, accounts, apps, assets, sp, acct1.Address, + nil, types.Digest{}, [32]byte{}, types.Address{}, + ) + if err != nil { + log.Fatalf("Failed to create app call txn: %+v", err) + } + + _, stxn, err := crypto.SignTransaction(acct1.PrivateKey, appCallTxn) + if err != nil { + log.Fatalf("Failed to sign app txn: %+v", err) + } + + signedAppCallTxn := types.SignedTxn{} + msgpack.Decode(stxn, &signedAppCallTxn) + + drr, err := transaction.CreateDryrun(algodClient, []types.SignedTxn{signedAppCallTxn}, nil, context.Background()) + if err != nil { + log.Fatalf("Failed to create dryrun: %+v", err) + } + + os.WriteFile("dryrun.msgp", msgpack.Encode(drr), 0666) + // example: DEBUG_DRYRUN_DUMP + + // example: DEBUG_DRYRUN_SUBMIT + // Create the dryrun request object + drReq, err := transaction.CreateDryrun(algodClient, []types.SignedTxn{signedAppCallTxn}, nil, context.Background()) + if err != nil { + log.Fatalf("Failed to create dryrun: %+v", err) + } + + // Pass dryrun request to algod server + dryrunResponse, err := algodClient.TealDryrun(drReq).Do(context.Background()) + if err != nil { + log.Fatalf("failed to dryrun request: %s", err) + } + + // Inspect the response to check result + for _, txn := range dryrunResponse.Txns { + log.Printf("%+v", txn.AppCallTrace) + } + // example: DEBUG_DRYRUN_SUBMIT + os.Remove("dryrun.msgp") +} diff --git a/examples/gen-addresses/main.go b/examples/gen-addresses/main.go deleted file mode 100644 index 90301ee6..00000000 --- a/examples/gen-addresses/main.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "github.com/algorand/go-algorand-sdk/v2/transaction" - "strings" - - "github.com/algorand/go-algorand-sdk/v2/client/kmd" - "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" - "github.com/algorand/go-algorand-sdk/v2/crypto" - "github.com/algorand/go-algorand-sdk/v2/types" -) - -// CHANGE ME -const ( - exampleWalletName = "unencrypted-default-wallet" - exampleWalletPassword = "" - exampleWalletDriver = kmd.DefaultWalletDriver -) - -var ( - kmdAddress = "http://localhost:4002" - kmdToken = strings.Repeat("a", 64) - - algodAddress = "http://localhost:4001" - algodToken = strings.Repeat("a", 64) -) - -func main() { - // Create a kmd client - kmdClient, err := kmd.MakeClient(kmdAddress, kmdToken) - if err != nil { - fmt.Printf("failed to make kmd client: %s\n", err) - return - } - fmt.Println("Made a kmd client") - - // Create an algod client - algodClient, err := algod.MakeClient(algodAddress, algodToken) - if err != nil { - fmt.Printf("failed to make algod client: %s\n", err) - return - } - - // Print algod status - nodeStatus, err := algodClient.Status().Do(context.Background()) - if err != nil { - fmt.Printf("error getting algod status: %s\n", err) - return - } - fmt.Printf("algod last round: %d\n", nodeStatus.LastRound) - - // List existing wallets, and check if our example wallet already exists - resp0, err := kmdClient.ListWallets() - if err != nil { - fmt.Printf("error listing wallets: %s\n", err) - return - } - fmt.Printf("Got %d wallet(s):\n", len(resp0.Wallets)) - var exampleExists bool - var exampleWalletID string - for _, wallet := range resp0.Wallets { - fmt.Printf("ID: %s\tName: %s\n", wallet.ID, wallet.Name) - if wallet.Name == exampleWalletName { - exampleWalletID = wallet.ID - exampleExists = true - } - } - - // Create the example wallet, if it doesn't already exist - if !exampleExists { - resp1, err := kmdClient.CreateWallet(exampleWalletName, exampleWalletPassword, exampleWalletDriver, types.MasterDerivationKey{}) - if err != nil { - fmt.Printf("error creating wallet: %s\n", err) - return - } - exampleWalletID = resp1.Wallet.ID - fmt.Printf("Created wallet '%s' with ID: %s\n", resp1.Wallet.Name, exampleWalletID) - } - - // Get a wallet handle - resp2, err := kmdClient.InitWalletHandle(exampleWalletID, exampleWalletPassword) - if err != nil { - fmt.Printf("Error initializing wallet: %s\n", err) - return - } - - // Extract the wallet handle - exampleWalletHandleToken := resp2.WalletHandleToken - - // Generate some addresses in the wallet - fmt.Println("Generating 10 addresses") - var addresses []string - for i := 0; i < 10; i++ { - resp3, err := kmdClient.GenerateKey(exampleWalletHandleToken) - if err != nil { - fmt.Printf("Error generating key: %s\n", err) - return - } - fmt.Printf("Generated address %s\n", resp3.Address) - addresses = append(addresses, resp3.Address) - } - - // Extract the private key of the first address - fmt.Printf("Extracting private key for %s\n", addresses[0]) - resp4, err := kmdClient.ExportKey(exampleWalletHandleToken, exampleWalletPassword, addresses[0]) - if err != nil { - fmt.Printf("Error extracting secret key: %s\n", err) - return - } - privateKey := resp4.PrivateKey - - // Get the suggested transaction parameters - txParams, err := algodClient.SuggestedParams().Do(context.Background()) - if err != nil { - fmt.Printf("error getting suggested tx params: %s\n", err) - return - } - - tx, err := transaction.MakePaymentTxn(addresses[0], addresses[1], 100, nil, "", txParams) - if err != nil { - fmt.Printf("Error creating transaction: %s\n", err) - return - } - fmt.Printf("Made unsigned transaction: %+v\n", tx) - fmt.Println("Signing transaction with go-algo-sdk library function (not kmd)") - - txid, stx, err := crypto.SignTransaction(privateKey, tx) - if err != nil { - fmt.Printf("Failed to sign transaction: %s\n", err) - return - } - - fmt.Printf("Made signed transaction with TxID %s: %x\n", txid, stx) - - // Sign the same transaction with kmd - fmt.Println("Signing same transaction with kmd") - resp5, err := kmdClient.SignTransaction(exampleWalletHandleToken, exampleWalletPassword, tx) - if err != nil { - fmt.Printf("Failed to sign transaction with kmd: %s\n", err) - return - } - - fmt.Printf("kmd made signed transaction with bytes: %x\n", resp5.SignedTransaction) - if bytes.Equal(resp5.SignedTransaction, stx) { - fmt.Println("signed transactions match!") - } else { - fmt.Println("signed transactions don't match!") - } -} diff --git a/examples/indexer/main.go b/examples/indexer/main.go new file mode 100644 index 00000000..2a856ac6 --- /dev/null +++ b/examples/indexer/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/client/v2/indexer" + "github.com/algorand/go-algorand-sdk/v2/examples" +) + +func main() { + // example: INDEXER_CREATE_CLIENT + // Create a new indexer client, configured to connect to out local sandbox + var indexerAddress = "http://localhost:8980" + var indexerToken = strings.Repeat("a", 64) + indexerClient, _ := indexer.MakeClient( + indexerAddress, + indexerToken, + ) + + // Or, if necessary, pass alternate headers + + var indexerHeader common.Header + indexerHeader.Key = "X-API-Key" + indexerHeader.Value = indexerToken + indexerClientWithHeaders, err := indexer.MakeClientWithHeaders( + indexerAddress, + indexerToken, + []*common.Header{&indexerHeader}, + ) + // example: INDEXER_CREATE_CLIENT + + // Suppress X declared but not used + _ = indexerClientWithHeaders + _ = indexerClient + + indexerClient = examples.GetIndexerClient() + + if err != nil { + fmt.Printf("failed to make indexer client: %s\n", err) + return + } + + indexerHealth, err := indexerClient.HealthCheck().Do(context.Background()) + if err != nil { + fmt.Printf("Failed to get status: %s\n", err) + return + } + + fmt.Printf("Indexer Round: %d\n", indexerHealth.Round) + + // example: INDEXER_LOOKUP_ASSET + // query parameters + var assetId uint64 = 2044572 + var minBalance uint64 = 50 + + // Lookup accounts with minimum balance of asset + assetResult, _ := indexerClient. + LookupAssetBalances(assetId). + CurrencyGreaterThan(minBalance). + Do(context.Background()) + + // Print the results + assetJson, _ := json.MarshalIndent(assetResult, "", "\t") + fmt.Printf(string(assetJson) + "\n") + // example: INDEXER_LOOKUP_ASSET + + assetJson = nil + + // example: INDEXER_SEARCH_MIN_AMOUNT + // query parameters + var transactionMinAmount uint64 = 10 + + // Query + transactionResult, _ := indexerClient. + SearchForTransactions(). + CurrencyGreaterThan(transactionMinAmount). + Do(context.Background()) + + // Print results + transactionJson, _ := json.MarshalIndent(transactionResult, "", "\t") + fmt.Printf(string(transactionJson) + "\n") + // example: INDEXER_SEARCH_MIN_AMOUNT + + // example: INDEXER_PAGINATE_RESULTS + var nextToken = "" + var numTx = 1 + var numPages = 1 + var pagedMinAmount uint64 = 10 + var limit uint64 = 1 + + for numTx > 0 { + // Query + pagedResults, err := indexerClient. + SearchForTransactions(). + CurrencyGreaterThan(pagedMinAmount). + Limit(limit). + NextToken(nextToken). + Do(context.Background()) + if err != nil { + return + } + pagedTransactions := pagedResults.Transactions + numTx = len(pagedTransactions) + nextToken = pagedResults.NextToken + + if numTx > 0 { + // Print results + pagedJson, err := json.MarshalIndent(pagedTransactions, "", "\t") + if err != nil { + return + } + fmt.Printf(string(pagedJson) + "\n") + fmt.Println("End of page : ", numPages) + fmt.Println("Transaction printed : ", len(pagedTransactions)) + fmt.Println("Next Token : ", nextToken) + numPages++ + } + } + // example: INDEXER_PAGINATE_RESULTS + + // example: INDEXER_PREFIX_SEARCH + // Parameters + var notePrefix = "showing prefix" + + // Query + prefixResult, _ := indexerClient. + SearchForTransactions(). + NotePrefix([]byte(notePrefix)). + Do(context.Background()) + + // Print results + prefixJson, _ := json.MarshalIndent(prefixResult, "", "\t") + fmt.Printf(string(prefixJson) + "\n") + // example: INDEXER_PREFIX_SEARCH +} diff --git a/examples/kmd/main.go b/examples/kmd/main.go new file mode 100644 index 00000000..acb57aa4 --- /dev/null +++ b/examples/kmd/main.go @@ -0,0 +1,232 @@ +package main + +import ( + "crypto/ed25519" + "fmt" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/kmd" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/mnemonic" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + var ( + exampleWalletID string + exampleWalletHandleToken string + genResponse kmd.GenerateKeyResponse + initResponse kmd.InitWalletHandleResponse + ) + + // example: KMD_CREATE_CLIENT + // Create a new kmd client, configured to connect to out local sandbox + var kmdAddress = "http://localhost:4002" + var kmdToken = strings.Repeat("a", 64) + kmdClient, err := kmd.MakeClient( + kmdAddress, + kmdToken, + ) + // example: KMD_CREATE_CLIENT + _ = kmdClient + kmdClient = examples.GetKmdClient() + + if err != nil { + fmt.Printf("failed to make kmd client: %s\n", err) + return + } + + // example: KMD_CREATE_WALLET + // Create the example wallet, if it doesn't already exist + createResponse, err := kmdClient.CreateWallet( + "DemoWallet", + "password", + kmd.DefaultWalletDriver, + types.MasterDerivationKey{}, + ) + if err != nil { + fmt.Printf("error creating wallet: %s\n", err) + return + } + + // We need the wallet ID in order to get a wallet handle, so we can add accounts + exampleWalletID = createResponse.Wallet.ID + fmt.Printf("Created wallet '%s' with ID: %s\n", createResponse.Wallet.Name, exampleWalletID) + // example: KMD_CREATE_WALLET + + // example: KMD_CREATE_ACCOUNT + // Get a wallet handle. + initResponse, _ = kmdClient.InitWalletHandle( + exampleWalletID, + "password", + ) + exampleWalletHandleToken = initResponse.WalletHandleToken + + // Generate a new address from the wallet handle + genResponse, err = kmdClient.GenerateKey(exampleWalletHandleToken) + if err != nil { + fmt.Printf("Error generating key: %s\n", err) + return + } + accountAddress := genResponse.Address + fmt.Printf("New Account: %s\n", accountAddress) + // example: KMD_CREATE_ACCOUNT + + // example: KMD_EXPORT_ACCOUNT + // Extract the account sk + accountKeyResponse, _ := kmdClient.ExportKey( + exampleWalletHandleToken, + "password", + accountAddress, + ) + accountKey := accountKeyResponse.PrivateKey + // Convert sk to mnemonic + mn, err := mnemonic.FromPrivateKey(accountKey) + if err != nil { + fmt.Printf("Error getting backup phrase: %s\n", err) + return + } + fmt.Printf("Account Mnemonic: %v ", mn) + // example: KMD_EXPORT_ACCOUNT + + // example: KMD_IMPORT_ACCOUNT + account := crypto.GenerateAccount() + fmt.Println("Account Address: ", account.Address) + mn, err = mnemonic.FromPrivateKey(account.PrivateKey) + if err != nil { + fmt.Printf("Error getting backup phrase: %s\n", err) + return + } + fmt.Printf("Account Mnemonic: %s\n", mn) + importedAccount, _ := kmdClient.ImportKey( + exampleWalletHandleToken, + account.PrivateKey, + ) + fmt.Println("Account Successfully Imported: ", importedAccount.Address) + // example: KMD_IMPORT_ACCOUNT + + // Get the MDK for Recovery example + backupResponse, err := kmdClient.ExportMasterDerivationKey(exampleWalletHandleToken, "password") + if err != nil { + fmt.Printf("error exporting mdk: %s\n", err) + return + } + backupPhrase, _ := mnemonic.FromMasterDerivationKey(backupResponse.MasterDerivationKey) + fmt.Printf("Backup: %s\n", backupPhrase) + + // example: KMD_RECOVER_WALLET + keyBytes, err := mnemonic.ToKey(backupPhrase) + if err != nil { + fmt.Printf("failed to get key: %s\n", err) + return + } + + var mdk types.MasterDerivationKey + copy(mdk[:], keyBytes) + recoverResponse, err := kmdClient.CreateWallet( + "RecoveryWallet", + "password", + kmd.DefaultWalletDriver, + mdk, + ) + if err != nil { + fmt.Printf("error creating wallet: %s\n", err) + return + } + + // We need the wallet ID in order to get a wallet handle, so we can add accounts + exampleWalletID = recoverResponse.Wallet.ID + fmt.Printf("Created wallet '%s' with ID: %s\n", recoverResponse.Wallet.Name, exampleWalletID) + + // Get a wallet handle. The wallet handle is used for things like signing transactions + // and creating accounts. Wallet handles do expire, but they can be renewed + initResponse, err = kmdClient.InitWalletHandle( + exampleWalletID, + "password", + ) + if err != nil { + fmt.Printf("Error initializing wallet handle: %s\n", err) + return + } + + // Extract the wallet handle + exampleWalletHandleToken = initResponse.WalletHandleToken + fmt.Printf("Got wallet handle: '%s'\n", exampleWalletHandleToken) + + // Generate a new address from the wallet handle + genResponse, err = kmdClient.GenerateKey(exampleWalletHandleToken) + if err != nil { + fmt.Printf("Error generating key: %s\n", err) + return + } + fmt.Printf("Recovered address %s\n", genResponse.Address) + // example: KMD_RECOVER_WALLET + + // example: ACCOUNT_GENERATE + newAccount := crypto.GenerateAccount() + passphrase, err := mnemonic.FromPrivateKey(newAccount.PrivateKey) + + if err != nil { + fmt.Printf("Error creating transaction: %s\n", err) + } else { + fmt.Printf("My address: %s\n", newAccount.Address) + fmt.Printf("My passphrase: %s\n", passphrase) + } + // example: ACCOUNT_GENERATE + + // example: MULTISIG_CREATE + // Get pre-defined set of keys for example + _, pks := loadAccounts() + addr1, _ := types.DecodeAddress(pks[1]) + addr2, _ := types.DecodeAddress(pks[2]) + addr3, _ := types.DecodeAddress(pks[3]) + + ma, err := crypto.MultisigAccountWithParams(1, 2, []types.Address{ + addr1, + addr2, + addr3, + }) + + if err != nil { + panic("invalid multisig parameters") + } + fromAddr, _ := ma.Address() + // Print multisig account + fmt.Printf("Multisig address : %s \n", fromAddr) + // example: MULTISIG_CREATE +} + +// Accounts to be used through examples +func loadAccounts() (map[int][]byte, map[int]string) { + // Shown for demonstration purposes. NEVER reveal secret mnemonics in practice. + // Change these values to use the accounts created previously. + // Paste in mnemonic phrases for all three accounts + acc1 := crypto.GenerateAccount() + mnemonic1, _ := mnemonic.FromPrivateKey(acc1.PrivateKey) + acc2 := crypto.GenerateAccount() + mnemonic2, _ := mnemonic.FromPrivateKey(acc2.PrivateKey) + acc3 := crypto.GenerateAccount() + mnemonic3, _ := mnemonic.FromPrivateKey(acc3.PrivateKey) + + mnemonics := []string{mnemonic1, mnemonic2, mnemonic3} + pks := map[int]string{1: "", 2: "", 3: ""} + var sks = make(map[int][]byte) + + for i, m := range mnemonics { + var err error + sk, err := mnemonic.ToPrivateKey(m) + sks[i+1] = sk + if err != nil { + fmt.Printf("Issue with account %d private key conversion.", i+1) + } + // derive public address from Secret Key. + pk := sk.Public() + var a types.Address + cpk := pk.(ed25519.PublicKey) + copy(a[:], cpk[:]) + pks[i+1] = a.String() + fmt.Printf("Loaded Key %d: %s\n", i+1, pks[i+1]) + } + return sks, pks +} diff --git a/examples/lsig/main.go b/examples/lsig/main.go new file mode 100644 index 00000000..528019ee --- /dev/null +++ b/examples/lsig/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/binary" + "io/ioutil" + "log" + + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +func main() { + algodClient := examples.GetAlgodClient() + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get sandbox accounts: %s", err) + } + seedAcct := accts[0] + + // example: LSIG_COMPILE + teal, err := ioutil.ReadFile("lsig/simple.teal") + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + + result, err := algodClient.TealCompile(teal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + lsigBinary, err := base64.StdEncoding.DecodeString(result.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + // example: LSIG_COMPILE + + // example: LSIG_INIT + lsig := crypto.LogicSigAccount{ + Lsig: types.LogicSig{Logic: lsigBinary, Args: nil}, + } + // example: LSIG_INIT + _ = lsig + + // example: LSIG_PASS_ARGS + encodedArg := make([]byte, 8) + binary.BigEndian.PutUint64(encodedArg, 123) + + lsigWithArgs := crypto.LogicSigAccount{ + Lsig: types.LogicSig{Logic: lsigBinary, Args: [][]byte{encodedArg}}, + } + // example: LSIG_PASS_ARGS + _ = lsigWithArgs + + // seed lsig so the pay from the lsig works + lsa, err := lsig.Address() + if err != nil { + log.Fatalf("failed to get lsig address: %s", err) + } + seedAddr := seedAcct.Address.String() + sp, _ := algodClient.SuggestedParams().Do(context.Background()) + txn, _ := transaction.MakePaymentTxn(seedAddr, lsa.String(), 1000000, nil, "", sp) + txid, stx, _ := crypto.SignTransaction(seedAcct.PrivateKey, txn) + algodClient.SendRawTransaction(stx).Do(context.Background()) + transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + + // example: LSIG_SIGN_FULL + sp, err = algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + + lsigAddr, err := lsig.Address() + if err != nil { + log.Fatalf("failed to get lsig address: %s", err) + } + ptxn, err := transaction.MakePaymentTxn(lsigAddr.String(), seedAddr, 10000, nil, "", sp) + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + txid, stxn, err := crypto.SignLogicSigAccountTransaction(lsig, ptxn) + if err != nil { + log.Fatalf("failed to sign transaction with lsig: %s", err) + } + _, err = algodClient.SendRawTransaction(stxn).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + payResult, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("failed while waiting for transaction: %s", err) + } + log.Printf("Lsig pay confirmed in round: %d", payResult.ConfirmedRound) + // example: LSIG_SIGN_FULL + + // example: LSIG_DELEGATE_FULL + // account signs the logic, and now the logic may be passed instead + // of a signature for a transaction + var args [][]byte + delSig, err := crypto.MakeLogicSigAccountDelegated(lsigBinary, args, seedAcct.PrivateKey) + if err != nil { + log.Fatalf("failed to make delegate lsig: %s", err) + } + + delSigPay, err := transaction.MakePaymentTxn(seedAddr, lsigAddr.String(), 10000, nil, "", sp) + if err != nil { + log.Fatalf("failed to make transaction: %s", err) + } + + delTxId, delStxn, err := crypto.SignLogicSigAccountTransaction(delSig, delSigPay) + if err != nil { + log.Fatalf("failed to sign with delegate sig: %s", err) + } + + _, err = algodClient.SendRawTransaction(delStxn).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + delPayResult, err := transaction.WaitForConfirmation(algodClient, delTxId, 4, context.Background()) + if err != nil { + log.Fatalf("failed while waiting for transaction: %s", err) + } + + log.Printf("Delegated Lsig pay confirmed in round: %d", delPayResult.ConfirmedRound) + // example: LSIG_DELEGATE_FULL +} diff --git a/examples/lsig/sample_arg.teal b/examples/lsig/sample_arg.teal new file mode 100644 index 00000000..8f21008e --- /dev/null +++ b/examples/lsig/sample_arg.teal @@ -0,0 +1,5 @@ +#pragma version 5 +arg_0 +btoi +int 123 +== \ No newline at end of file diff --git a/examples/lsig/simple.teal b/examples/lsig/simple.teal new file mode 100644 index 00000000..d6298656 --- /dev/null +++ b/examples/lsig/simple.teal @@ -0,0 +1,3 @@ +#pragma version 5 +int 1 +return \ No newline at end of file diff --git a/examples/overview/main.go b/examples/overview/main.go new file mode 100644 index 00000000..b6fc4cda --- /dev/null +++ b/examples/overview/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/client/v2/common" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + + // example: ALGOD_CREATE_CLIENT + // Create a new algod client, configured to connect to out local sandbox + var algodAddress = "http://localhost:4001" + var algodToken = strings.Repeat("a", 64) + algodClient, _ := algod.MakeClient( + algodAddress, + algodToken, + ) + + // Or, if necessary, pass alternate headers + + var algodHeader common.Header + algodHeader.Key = "X-API-Key" + algodHeader.Value = algodToken + algodClientWithHeaders, _ := algod.MakeClientWithHeaders( + algodAddress, + algodToken, + []*common.Header{&algodHeader}, + ) + + // Or, for better performance, pass a custom Transport, in this case allowing + // up to 100 simultaneous connections to the same host (ie: an algod node) + // Clone Go's default transport settings but increase connection values + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.MaxIdleConns = 100 + customTransport.MaxConnsPerHost = 100 + customTransport.MaxIdleConnsPerHost = 100 + + algodClientWithCustomTransport, _ := algod.MakeClientWithTransport( + algodAddress, + algodToken, + nil, // accepts additional headers like MakeClientWithHeaders + customTransport, + ) + // example: ALGOD_CREATE_CLIENT + + _ = algodClientWithCustomTransport + _ = algodClientWithHeaders + _ = algodClient + + // Override with the version that has correct port + algodClient = examples.GetAlgodClient() + + accts, err := examples.GetSandboxAccounts() + if err != nil { + log.Fatalf("failed to get accounts: %s", err) + } + acct := accts[0] + + // example: ALGOD_FETCH_ACCOUNT_INFO + acctInfo, err := algodClient.AccountInformation(acct.Address.String()).Do(context.Background()) + if err != nil { + log.Fatalf("failed to fetch account info: %s", err) + } + log.Printf("Account balance: %d microAlgos", acctInfo.Amount) + // example: ALGOD_FETCH_ACCOUNT_INFO + + // example: TRANSACTION_PAYMENT_CREATE + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to get suggested params: %s", err) + } + // payment from account to itself + ptxn, err := transaction.MakePaymentTxn(acct.Address.String(), acct.Address.String(), 100000, nil, "", sp) + if err != nil { + log.Fatalf("failed creating transaction: %s", err) + } + // example: TRANSACTION_PAYMENT_CREATE + + // example: TRANSACTION_PAYMENT_SIGN + _, sptxn, err := crypto.SignTransaction(acct.PrivateKey, ptxn) + if err != nil { + fmt.Printf("Failed to sign transaction: %s\n", err) + return + } + // example: TRANSACTION_PAYMENT_SIGN + + // example: TRANSACTION_PAYMENT_SUBMIT + pendingTxID, err := algodClient.SendRawTransaction(sptxn).Do(context.Background()) + if err != nil { + fmt.Printf("failed to send transaction: %s\n", err) + return + } + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, pendingTxID, 4, context.Background()) + if err != nil { + fmt.Printf("Error waiting for confirmation on txID: %s\n", pendingTxID) + return + } + fmt.Printf("Confirmed Transaction: %s in Round %d\n", pendingTxID, confirmedTxn.ConfirmedRound) + // example: TRANSACTION_PAYMENT_SUBMIT + + // example: SP_MIN_FEE + suggestedParams, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("failed to %s", err) + } + log.Printf("Min fee from suggested params: %d", suggestedParams.MinFee) + // example: SP_MIN_FEE + + // example: CONST_MIN_FEE + log.Printf("Min fee const: %d", transaction.MinTxnFee) + // example: CONST_MIN_FEE + + // example: TRANSACTION_FEE_OVERRIDE + // by using fee pooling and setting our fee to 2x min tx fee + // we can cover the fee for another transaction in the group + sp.Fee = 2 * transaction.MinTxnFee + sp.FlatFee = true + // ... + // example: TRANSACTION_FEE_OVERRIDE + +} +func exampleAlgod() { +} diff --git a/examples/participation/main.go b/examples/participation/main.go new file mode 100644 index 00000000..fef59af8 --- /dev/null +++ b/examples/participation/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + + "github.com/algorand/go-algorand-sdk/v2/examples" + "github.com/algorand/go-algorand-sdk/v2/transaction" +) + +func main() { + markOnline() +} + +func markOnline() { + // setup connection + algodClient := examples.GetAlgodClient() + + // get network suggested parameters + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + fmt.Printf("error getting suggested tx params: %s\n", err) + return + } + + // Mark Account as "Online" (participating) + // example: TRANSACTION_KEYREG_ONLINE_CREATE + fromAddr := "MWAPNXBDFFD2V5KWXAHWKBO7FO4JN36VR4CIBDKDDE7WAUAGZIXM3QPJW4" + voteKey := "87iBW46PP4BpTDz6+IEGvxY6JqEaOtV0g+VWcJqoqtc=" + selKey := "1V2BE2lbFvS937H7pJebN0zxkqe1Nrv+aVHDTPbYRlw=" + sProofKey := "f0CYOA4yXovNBFMFX+1I/tYVBaAl7VN6e0Ki5yZA3H6jGqsU/LYHNaBkMQ/rN4M4F3UmNcpaTmbVbq+GgDsrhQ==" + voteFirst := uint64(16532750) + voteLast := uint64(19532750) + keyDilution := uint64(1732) + nonpart := false + tx, err := transaction.MakeKeyRegTxnWithStateProofKey( + fromAddr, + []byte{}, + sp, + voteKey, + selKey, + sProofKey, + voteFirst, + voteLast, + keyDilution, + nonpart, + ) + // example: TRANSACTION_KEYREG_ONLINE_CREATE + if err != nil { + fmt.Printf("Error creating transaction: %s\n", err) + return + } + _ = tx + + // disabled example: TRANSACTION_KEYREG_OFFLINE_CREATE + // disabled example: TRANSACTION_KEYREG_OFFLINE_CREATE +} diff --git a/examples/smoke_test.sh b/examples/smoke_test.sh new file mode 100755 index 00000000..68a0a9d7 --- /dev/null +++ b/examples/smoke_test.sh @@ -0,0 +1,19 @@ +#!/bin/env/bin bash + +export ALGOD_PORT="60000" +export INDEXER_PORT="59999" +export KMD_PORT="60001" + +# Loop through each directory in the current working directory +for dir in */; do + # Check if main.go exists in the directory + if [ -f "${dir}main.go" ]; then + # Run the "go run" command with the relative path + go run "${dir}main.go" + # Check if the test failed + if [ $? -ne 0 ]; then + echo "Test failed, stopping script" + exit 1 + fi + fi +done \ No newline at end of file diff --git a/examples/utils.go b/examples/utils.go new file mode 100644 index 00000000..eaa54e13 --- /dev/null +++ b/examples/utils.go @@ -0,0 +1,233 @@ +package examples + +import ( + "context" + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/algorand/go-algorand-sdk/v2/client/kmd" + "github.com/algorand/go-algorand-sdk/v2/client/v2/algod" + "github.com/algorand/go-algorand-sdk/v2/client/v2/indexer" + "github.com/algorand/go-algorand-sdk/v2/crypto" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "github.com/algorand/go-algorand-sdk/v2/types" +) + +var ( + ALGOD_ADDRESS = "http://localhost" + ALGOD_PORT = "4001" + ALGOD_URL = "" + ALGOD_TOKEN = strings.Repeat("a", 64) + + INDEXER_ADDRESS = "http://localhost" + INDEXER_PORT = "8980" + INDEXER_TOKEN = strings.Repeat("a", 64) + INDEXER_URL = "" + + KMD_ADDRESS = "http://localhost" + KMD_PORT = "4002" + KMD_TOKEN = strings.Repeat("a", 64) + KMD_URL = "" + + KMD_WALLET_NAME = "unencrypted-default-wallet" + KMD_WALLET_PASSWORD = "" +) + +func init() { + if aport, ok := os.LookupEnv("ALGOD_PORT"); ok { + ALGOD_PORT = aport + } + ALGOD_URL = fmt.Sprintf("%s:%s", ALGOD_ADDRESS, ALGOD_PORT) + + if iport, ok := os.LookupEnv("INDEXER_PORT"); ok { + INDEXER_PORT = iport + } + INDEXER_URL = fmt.Sprintf("%s:%s", INDEXER_ADDRESS, INDEXER_PORT) + + if kport, ok := os.LookupEnv("KMD_PORT"); ok { + KMD_PORT = kport + } + KMD_URL = fmt.Sprintf("%s:%s", KMD_ADDRESS, KMD_PORT) +} + +func GetAlgodClient() *algod.Client { + algodClient, err := algod.MakeClient( + ALGOD_URL, + ALGOD_TOKEN, + ) + + if err != nil { + log.Fatalf("Failed to create algod client: %s", err) + } + + return algodClient +} + +func GetKmdClient() kmd.Client { + kmdClient, err := kmd.MakeClient( + KMD_URL, + KMD_TOKEN, + ) + + if err != nil { + log.Fatalf("Failed to create kmd client: %s", err) + } + + return kmdClient +} + +func GetIndexerClient() *indexer.Client { + indexerClient, err := indexer.MakeClient( + INDEXER_URL, + INDEXER_TOKEN, + ) + + if err != nil { + log.Fatalf("Failed to create indexer client: %s", err) + } + + return indexerClient +} + +func GetSandboxAccounts() ([]crypto.Account, error) { + client := GetKmdClient() + + resp, err := client.ListWallets() + if err != nil { + return nil, fmt.Errorf("Failed to list wallets: %+v", err) + } + + var walletId string + for _, wallet := range resp.Wallets { + if wallet.Name == KMD_WALLET_NAME { + walletId = wallet.ID + } + } + + if walletId == "" { + return nil, fmt.Errorf("No wallet named %s", KMD_WALLET_NAME) + } + + whResp, err := client.InitWalletHandle(walletId, KMD_WALLET_PASSWORD) + if err != nil { + return nil, fmt.Errorf("Failed to init wallet handle: %+v", err) + } + + addrResp, err := client.ListKeys(whResp.WalletHandleToken) + if err != nil { + return nil, fmt.Errorf("Failed to list keys: %+v", err) + } + + var accts []crypto.Account + for _, addr := range addrResp.Addresses { + expResp, err := client.ExportKey(whResp.WalletHandleToken, KMD_WALLET_PASSWORD, addr) + if err != nil { + return nil, fmt.Errorf("Failed to export key: %+v", err) + } + + acct, err := crypto.AccountFromPrivateKey(expResp.PrivateKey) + if err != nil { + return nil, fmt.Errorf("Failed to create account from private key: %+v", err) + } + + accts = append(accts, acct) + } + + return accts, nil +} + +func CompileTeal(algodClient *algod.Client, path string) []byte { + teal, err := ioutil.ReadFile(path) + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + + result, err := algodClient.TealCompile(teal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + bin, err := base64.StdEncoding.DecodeString(result.Result) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + return bin +} + +func DeployApp(algodClient *algod.Client, creator crypto.Account) uint64 { + + var ( + approvalBinary = make([]byte, 1000) + clearBinary = make([]byte, 1000) + ) + + // Compile approval program + approvalTeal, err := ioutil.ReadFile("calculator/approval.teal") + if err != nil { + log.Fatalf("failed to read approval program: %s", err) + } + + approvalResult, err := algodClient.TealCompile(approvalTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + _, err = base64.StdEncoding.Decode(approvalBinary, []byte(approvalResult.Result)) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + + // Compile clear program + clearTeal, err := ioutil.ReadFile("calculator/clear.teal") + if err != nil { + log.Fatalf("failed to read clear program: %s", err) + } + + clearResult, err := algodClient.TealCompile(clearTeal).Do(context.Background()) + if err != nil { + log.Fatalf("failed to compile program: %s", err) + } + + _, err = base64.StdEncoding.Decode(clearBinary, []byte(clearResult.Result)) + if err != nil { + log.Fatalf("failed to decode compiled program: %s", err) + } + + // Create application + sp, err := algodClient.SuggestedParams().Do(context.Background()) + if err != nil { + log.Fatalf("error getting suggested tx params: %s", err) + } + + txn, err := transaction.MakeApplicationCreateTx( + false, approvalBinary, clearBinary, + types.StateSchema{}, types.StateSchema{}, + nil, nil, nil, nil, + sp, creator.Address, nil, + types.Digest{}, [32]byte{}, types.ZeroAddress, + ) + if err != nil { + log.Fatalf("failed to make txn: %s", err) + } + + txid, stx, err := crypto.SignTransaction(creator.PrivateKey, txn) + if err != nil { + log.Fatalf("failed to sign transaction: %s", err) + } + + _, err = algodClient.SendRawTransaction(stx).Do(context.Background()) + if err != nil { + log.Fatalf("failed to send transaction: %s", err) + } + + confirmedTxn, err := transaction.WaitForConfirmation(algodClient, txid, 4, context.Background()) + if err != nil { + log.Fatalf("error waiting for confirmation: %s", err) + } + + return confirmedTxn.ApplicationIndex +} diff --git a/test/algodclientv2_test.go b/test/algodclientv2_test.go index 030f2ce5..def58c4c 100644 --- a/test/algodclientv2_test.go +++ b/test/algodclientv2_test.go @@ -55,10 +55,12 @@ func AlgodClientV2Context(s *godog.Suite) { s.Step(`^we make a GetStateProof call for round (\d+)$`, weMakeAGetStateProofCallForRound) s.Step(`^we make a GetTransactionProof call for round (\d+) txid "([^"]*)" and hashtype "([^"]*)"$`, weMakeAGetTransactionProofCallForRoundTxidAndHashtype) s.Step(`^we make a Lookup Block Hash call against round (\d+)$`, weMakeALookupBlockHashCallAgainstRound) - s.Step(`^we make a GetLedgerStateDelta call against round (\d+)$`, weMakeAGetLedgerStateDeltaCallAgainstRound) + s.Step(`^we make a Ready call$`, weMakeAReadyCall) s.Step(`^we make a SetSyncRound call against round (\d+)$`, weMakeASetSyncRoundCallAgainstRound) s.Step(`^we make a GetSyncRound call$`, weMakeAGetSyncRoundCall) s.Step(`^we make a UnsetSyncRound call$`, weMakeAUnsetSyncRoundCall) + s.Step(`^we make a SetBlockTimeStampOffset call against offset (\d+)$`, weMakeASetBlockTimeStampOffsetCallAgainstOffset) + s.Step(`^we make a GetBlockTimeStampOffset call$`, weMakeAGetBlockTimeStampOffsetCall) s.BeforeScenario(func(interface{}) { globalErrForExamination = nil @@ -310,12 +312,12 @@ func weMakeALookupBlockHashCallAgainstRound(round int) error { return nil } -func weMakeAGetLedgerStateDeltaCallAgainstRound(round int) error { +func weMakeAReadyCall() error { algodClient, err := algod.MakeClient(mockServer.URL, "") if err != nil { return err } - algodClient.GetLedgerStateDelta(uint64(round)).Do(context.Background()) + algodClient.GetReady().Do(context.Background()) return nil } @@ -345,3 +347,20 @@ func weMakeAUnsetSyncRoundCall() error { algodClient.UnsetSyncRound().Do(context.Background()) return nil } +func weMakeASetBlockTimeStampOffsetCallAgainstOffset(offset int) error { + algodClient, err := algod.MakeClient(mockServer.URL, "") + if err != nil { + return err + } + algodClient.SetBlockTimeStampOffset(uint64(offset)).Do(context.Background()) + return nil +} + +func weMakeAGetBlockTimeStampOffsetCall() error { + algodClient, err := algod.MakeClient(mockServer.URL, "") + if err != nil { + return err + } + algodClient.GetBlockTimeStampOffset().Do(context.Background()) + return nil +} diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index f7f70745..3e2b33ee 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -7,4 +7,4 @@ COPY . $HOME/go-algorand-sdk WORKDIR $HOME/go-algorand-sdk # Run integration tests -CMD ["/bin/bash", "-c", "make unit && make integration"] +CMD ["/bin/bash", "-c", "make unit && make integration && make smoke-test-examples"] diff --git a/test/integration.tags b/test/integration.tags index 91dace71..f8b49f9f 100644 --- a/test/integration.tags +++ b/test/integration.tags @@ -6,6 +6,7 @@ @auction @c2c @compile +@compile.disassemble @compile.sourcemap @dryrun @kmd diff --git a/test/responses_unit_test.go b/test/responses_unit_test.go index c98c234b..7b9a6d28 100644 --- a/test/responses_unit_test.go +++ b/test/responses_unit_test.go @@ -172,9 +172,6 @@ func weMakeAnyCallTo(client /* algod/indexer */, endpoint string) (err error) { case "GetBlockHash": response, err = algodC.GetBlockHash(123).Do(context.Background()) - case "GetLedgerStateDelta": - response, err = - algodC.GetLedgerStateDelta(123).Do(context.Background()) case "UnsetSyncRound": response, err = algodC.UnsetSyncRound().Do(context.Background()) @@ -184,6 +181,9 @@ func weMakeAnyCallTo(client /* algod/indexer */, endpoint string) (err error) { case "GetSyncRound": response, err = algodC.GetSyncRound().Do(context.Background()) + case "GetBlockTimeStampOffset": + response, err = + algodC.GetBlockTimeStampOffset().Do(context.Background()) case "any": // This is an error case // pickup the error as the response @@ -216,7 +216,7 @@ func theParsedResponseShouldEqualTheMockResponse() error { if responseStr, ok := response.(string); ok { responseJson = responseStr } else { - responseJson = string(json.Encode(response)) + responseJson = string(json.EncodeStrict(response)) } } diff --git a/test/steps_test.go b/test/steps_test.go index 7945182a..072943b1 100644 --- a/test/steps_test.go +++ b/test/steps_test.go @@ -10,7 +10,6 @@ import ( "encoding/json" "flag" "fmt" - "github.com/algorand/go-algorand-sdk/v2/transaction" "os" "path" "reflect" @@ -19,6 +18,8 @@ import ( "testing" "time" + "github.com/algorand/go-algorand-sdk/v2/transaction" + "golang.org/x/crypto/ed25519" "github.com/algorand/go-algorand-sdk/v2/abi" @@ -352,6 +353,7 @@ func FeatureContext(s *godog.Suite) { s.Step(`^a base64 encoded program bytes for heuristic sanity check "([^"]*)"$`, takeB64encodedBytes) s.Step(`^I start heuristic sanity check over the bytes$`, heuristicCheckOverBytes) s.Step(`^if the heuristic sanity check throws an error, the error contains "([^"]*)"$`, checkErrorIfMatching) + s.Step(`^disassembly of "([^"]*)" matches "([^"]*)"$`, disassemblyMatches) s.BeforeScenario(func(interface{}) { stxObj = types.SignedTxn{} @@ -2559,3 +2561,23 @@ func checkErrorIfMatching(errMsg string) error { } return nil } + +func disassemblyMatches(bytecodeFilename, sourceFilename string) error { + disassembledBytes, err := loadResource(bytecodeFilename) + if err != nil { + return err + } + actualResult, err := aclv2.TealDisassemble(disassembledBytes).Do(context.Background()) + if err != nil { + return err + } + expectedBytes, err := loadResource(sourceFilename) + if err != nil { + return err + } + expectedResult := string(expectedBytes) + if actualResult.Result != expectedResult { + return fmt.Errorf("Actual program does not match expected: %s != %s", actualResult.Result, expectedResult) + } + return nil +} diff --git a/test/unit.tags b/test/unit.tags index cb55c5bf..24c201b2 100644 --- a/test/unit.tags +++ b/test/unit.tags @@ -1,11 +1,11 @@ @unit.abijson @unit.abijson.byname @unit.algod -@unit.blocksummary @unit.algod.ledger_refactoring @unit.applications @unit.applications.boxes @unit.atomic_transaction_composer +@unit.blocksummary @unit.dryrun @unit.dryrun.trace.application @unit.feetest @@ -15,6 +15,7 @@ @unit.indexer.rekey @unit.offline @unit.program_sanity_check +@unit.ready @unit.rekey @unit.responses @unit.responses.231 @@ -23,14 +24,15 @@ @unit.responses.messagepack @unit.responses.messagepack.231 @unit.responses.participationupdates +@unit.responses.sync +@unit.responses.timestamp @unit.responses.unlimited_assets -@unit.responses.statedelta @unit.sourcemap @unit.stateproof.paths @unit.stateproof.responses -@unit.stateproof.responses.msgp -@unit.statedelta +@unit.sync @unit.tealsign +@unit.timestamp @unit.transactions @unit.transactions.keyreg @unit.transactions.payment diff --git a/test/utilities.go b/test/utilities.go index 6f64822c..26fda1b9 100644 --- a/test/utilities.go +++ b/test/utilities.go @@ -15,6 +15,7 @@ import ( sdk_json "github.com/algorand/go-algorand-sdk/v2/encoding/json" "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/v2/types" ) func VerifyResponse(expectedFile string, actual string) error { @@ -43,7 +44,7 @@ func VerifyResponse(expectedFile string, actual string) error { if err != nil { return fmt.Errorf("failed to decode '%s' from message pack: %v", expectedFile, err) } - expectedString = string(sdk_json.Encode(generic)) + expectedString = string(sdk_json.EncodeStrict(generic)) } err = EqualJson2(expectedString, actual) @@ -60,10 +61,16 @@ func VerifyResponse(expectedFile string, actual string) error { // For reference: j1 is the baseline, j2 is the test func EqualJson2(j1, j2 string) (err error) { var expected map[string]interface{} - json.Unmarshal([]byte(j1), &expected) + err = json.Unmarshal([]byte(j1), &expected) + if err != nil { + return fmt.Errorf("failed to unmarshal expected: %w", err) + } var actual map[string]interface{} - json.Unmarshal([]byte(j2), &actual) + err = json.Unmarshal([]byte(j2), &actual) + if err != nil { + return fmt.Errorf("failed to unmarshal actual: %w", err) + } err = recursiveCompare("root", expected, actual) @@ -113,18 +120,57 @@ func getType(val interface{}) ValueType { // The decoding process doesn't seem to distinguish between string and binary, but the encoding process // does. So sometimes the string will be base64 encoded and need to compare against the decoded string // value. +// There are some discrepancies in different algod / SDK types that causes +// encodings that need special handling: +// * Address is sometimes B32 encoded and sometimes B64 encoded. +// * BlockHash is sometimes B32 encoded (with a blk prefix) and sometimes B64 encoded. func binaryOrStringEqual(s1, s2 string) bool { if s1 == s2 { return true } - if val, err := base64.StdEncoding.DecodeString(s1); err == nil { - if string(val) == s2 { - return true + // S1 convert to S2 + { + if val, err := base64.StdEncoding.DecodeString(s1); err == nil { + if string(val) == s2 { + return true + } + var addr types.Address + if len(val) == len(addr[:]) { + copy(addr[:], val) + if addr.String() == s2 { + return true + } + } + + } + // parse blockhash + var bh types.BlockHash + if bh.UnmarshalText([]byte(s1)) == nil { + if base64.StdEncoding.EncodeToString(bh[:]) == s2 { + return true + } } } - if val, err := base64.StdEncoding.DecodeString(s2); err == nil { - if string(val) == s1 { - return true + + // S2 convert to S1 + { + if val, err := base64.StdEncoding.DecodeString(s2); err == nil { + if string(val) == s1 { + return true + } + var addr types.Address + if len(val) == len(addr[:]) { + copy(addr[:], val) + if addr.String() == s1 { + return true + } + } + } + var bh types.BlockHash + if bh.UnmarshalText([]byte(s2)) == nil { + if base64.StdEncoding.EncodeToString(bh[:]) == s1 { + return true + } } } return false diff --git a/types/address.go b/types/address.go index 7f26b290..1ac5a88c 100644 --- a/types/address.go +++ b/types/address.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha512" "encoding/base32" + "encoding/base64" ) const ( @@ -34,6 +35,34 @@ func (a Address) IsZero() bool { return a == ZeroAddress } +// MarshalText returns the address string as an array of bytes +func (addr *Address) MarshalText() ([]byte, error) { + result := base64.StdEncoding.EncodeToString(addr[:]) + return []byte(result), nil +} + +// UnmarshalText initializes the Address from an array of bytes. +// The bytes may be in the base32 checksum format, or the raw bytes base64 encoded. +func (addr *Address) UnmarshalText(text []byte) error { + address, err := DecodeAddress(string(text)) + if err == nil { + *addr = address + return nil + } + // ignore the DecodeAddress error because it isn't the native MarshalText format. + + // Check if its b64 encoded + data, err := base64.StdEncoding.DecodeString(string(text)) + if err == nil { + if len(data) != len(addr[:]) { + return errWrongAddressLen + } + copy(addr[:], data[:]) + return nil + } + return err +} + // DecodeAddress turns a checksum address string into an Address object. It // checks that the checksum is correct, and returns an error if it's not. func DecodeAddress(addr string) (a Address, err error) { diff --git a/types/address_test.go b/types/address_test.go index cbc6f17f..dee6c278 100644 --- a/types/address_test.go +++ b/types/address_test.go @@ -2,9 +2,12 @@ package types import ( "crypto/rand" + "fmt" "testing" "github.com/stretchr/testify/require" + + json2 "github.com/algorand/go-algorand-sdk/v2/encoding/json" ) func randomBytes(s []byte) { @@ -37,3 +40,55 @@ func TestGoldenValues(t *testing.T) { } require.Equal(t, golden, a.String()) } + +func TestUnmarshalAddress(t *testing.T) { + testcases := []struct { + name string + input string + str string + output string + err string + }{ + { + name: "B32+Checksum", + input: "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDE", + str: "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDE", + output: "+dITRRZHzDXzUnJagGfrT8gSGUYnZd1DNwWHdxxqYMs=", + }, { + name: "B64", + input: "+dITRRZHzDXzUnJagGfrT8gSGUYnZd1DNwWHdxxqYMs=", + str: "7HJBGRIWI7GDL42SOJNIAZ7LJ7EBEGKGE5S52QZXAWDXOHDKMDFR6AUXDE", + output: "+dITRRZHzDXzUnJagGfrT8gSGUYnZd1DNwWHdxxqYMs=", + }, { + name: "B64-err-length", + input: "AAE=", + err: "decoded address is the wrong length", + }, { + name: "B64-err-illegal", + input: "bogus", + err: "illegal base64 data at input byte 4", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // wrap in quotes so that it is valid JSON + data := []byte(fmt.Sprintf("\"%s\"", tc.input)) + + var addr Address + err := json2.Decode(data, &addr) + if tc.err != "" { + require.Error(t, err) + require.ErrorContains(t, err, tc.err) + return + } + require.NoError(t, err) + require.Equal(t, tc.str, addr.String()) + actual, err := addr.MarshalText() + require.NoError(t, err) + require.Equal(t, tc.output, string(actual)) + }) + } +} diff --git a/types/basics.go b/types/basics.go index 37ff4672..33c40eb1 100644 --- a/types/basics.go +++ b/types/basics.go @@ -1,7 +1,10 @@ package types import ( + "encoding/base32" "encoding/base64" + "errors" + "fmt" "math" "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" @@ -99,3 +102,17 @@ func (block *Block) FromBase64String(b64string string) error { } return nil } + +// DigestFromString converts a string to a Digest +func DigestFromString(str string) (d Digest, err error) { + decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(str) + if err != nil { + return d, err + } + if len(decoded) != len(d) { + msg := fmt.Sprintf(`Attempted to decode a string which was not a Digest: "%v"`, str) + return d, errors.New(msg) + } + copy(d[:], decoded[:]) + return d, err +} diff --git a/types/blockhash.go b/types/blockhash.go new file mode 100644 index 00000000..3bd2b97a --- /dev/null +++ b/types/blockhash.go @@ -0,0 +1,40 @@ +package types + +import ( + "encoding/base64" + "strings" +) + +// MarshalText returns the BlockHash string as an array of bytes +func (b *BlockHash) MarshalText() ([]byte, error) { + result := base64.StdEncoding.EncodeToString(b[:]) + return []byte(result), nil +} + +// UnmarshalText initializes the BlockHash from an array of bytes. +func (b *BlockHash) UnmarshalText(text []byte) error { + // Remove the blk- prefix if it is present to allow decoding either format. + if strings.HasPrefix(string(text), "blk-") { + text = text[4:] + } + + // Attempt to decode base32 format + d, err := DigestFromString(string(text)) + if err == nil { + *b = BlockHash(d) + return nil + } + + // ignore the DigestFromString error because it isn't the native MarshalText format. + + // Attempt to decode base64 format + data, err := base64.StdEncoding.DecodeString(string(text)) + if err == nil { + if len(data) != len(b[:]) { + return errWrongBlockHashLen + } + copy(b[:], data[:]) + return nil + } + return err +} diff --git a/types/blockhash_test.go b/types/blockhash_test.go new file mode 100644 index 00000000..dc9a36f7 --- /dev/null +++ b/types/blockhash_test.go @@ -0,0 +1,66 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + json2 "github.com/algorand/go-algorand-sdk/v2/encoding/json" +) + +func TestUnmarshalBlockHash(t *testing.T) { + testcases := []struct { + name string + input string + outputB64 string + err string + }{ + { + name: "blk-B32", + input: "blk-PEMLJXJYPLIFJ7DGVGTSEGUHUFR3M6Y67UZW3AC4LLNLF26XIXQA", + outputB64: "eRi03Th60FT8ZqmnIhqHoWO2ex79M22AXFrasuvXReA=", + }, { + name: "B32", + input: "PEMLJXJYPLIFJ7DGVGTSEGUHUFR3M6Y67UZW3AC4LLNLF26XIXQA", + outputB64: "eRi03Th60FT8ZqmnIhqHoWO2ex79M22AXFrasuvXReA=", + }, { + name: "B64", + input: "eRi03Th60FT8ZqmnIhqHoWO2ex79M22AXFrasuvXReA=", + outputB64: "eRi03Th60FT8ZqmnIhqHoWO2ex79M22AXFrasuvXReA=", + }, { + name: "B64-err-length", + input: "AAE=", + err: "decoded block hash is the wrong length", + }, { + name: "B64-err-illegal", + input: "bogus", + err: "illegal base64 data at input byte 4", + }, { + name: "Overflow does not panic", + input: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ", + err: "illegal base64 data at input byte 56", + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // wrap in quotes so that it is valid JSON + data := []byte(fmt.Sprintf("\"%s\"", tc.input)) + + var hash BlockHash + err := json2.Decode(data, &hash) + if tc.err != "" { + require.Error(t, err) + require.ErrorContains(t, err, tc.err) + return + } + require.NoError(t, err) + actual, err := hash.MarshalText() + require.NoError(t, err) + require.Equal(t, tc.outputB64, string(actual)) + }) + } +} diff --git a/types/errors.go b/types/errors.go index 551d5854..7a345d0f 100644 --- a/types/errors.go +++ b/types/errors.go @@ -7,3 +7,4 @@ import ( var errWrongAddressByteLen = fmt.Errorf("encoding address is the wrong length, should be %d bytes", hashLenBytes) var errWrongAddressLen = fmt.Errorf("decoded address is the wrong length, should be %d bytes", hashLenBytes+checksumLenBytes) var errWrongChecksum = fmt.Errorf("address checksum is incorrect, did you copy the address correctly?") +var errWrongBlockHashLen = fmt.Errorf("decoded block hash is the wrong length, should be %d bytes", Sha512_256Size) diff --git a/types/genesis.go b/types/genesis.go new file mode 100644 index 00000000..f7f95cd6 --- /dev/null +++ b/types/genesis.go @@ -0,0 +1,126 @@ +package types + +import ( + "crypto/sha512" + "fmt" + + "github.com/algorand/go-algorand-sdk/v2/encoding/msgpack" +) + +// GenesisHashID is the Genesis HashID defined in go-algorand/protocol/hash.go +var GenesisHashID = "GE" + +// A Genesis object defines an Algorand "universe" +type Genesis struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + // The SchemaID allows nodes to store data specific to a particular + // universe + SchemaID string `codec:"id"` + + // Network identifies the unique algorand network for which the ledger + // is valid. + Network string `codec:"network"` + + // Proto is the consensus protocol in use at the genesis block. + Proto string `codec:"proto"` + + // Allocation determines the initial accounts and their state. + Allocation []GenesisAllocation `codec:"alloc"` + + // RewardsPool is the address of the rewards pool. + RewardsPool string `codec:"rwd"` + + // FeeSink is the address of the fee sink. + FeeSink string `codec:"fees"` + + // Timestamp for the genesis block + Timestamp int64 `codec:"timestamp"` + + // Arbitrary genesis comment string - will be excluded from file if empty + Comment string `codec:"comment"` + + // DevMode defines whether this network operates in a developer mode or not. Developer mode networks + // are a single node network, that operates without the agreement service being active. In liue of the + // agreement service, a new block is generated each time a node receives a transaction group. The + // default value for this field is "false", which makes this field empty from it's encoding, and + // therefore backward compatible. + DevMode bool `codec:"devmode"` +} + +type GenesisAllocation struct { + _struct struct{} `codec:""` + + Address string `codec:"addr"` + Comment string `codec:"comment"` + State Account `codec:"state"` +} + +type Account struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Status byte `codec:"onl"` + MicroAlgos uint64 `codec:"algo"` + VoteID [32]byte `codec:"vote"` + SelectionID [32]byte `codec:"sel"` + StateProofID [64]byte `codec:"stprf"` + VoteLastValid uint64 `codec:"voteLst"` + VoteKeyDilution uint64 `codec:"voteKD"` +} + +// ID is the effective Genesis identifier - the combination +// of the network and the ledger schema version +func (genesis Genesis) ID() string { + return string(genesis.Network) + "-" + genesis.SchemaID +} + +// Hash is the genesis hash. +func (genesis Genesis) Hash() Digest { + hashRep := []byte(GenesisHashID) + data := msgpack.Encode(genesis) + hashRep = append(hashRep, data...) + return sha512.Sum512_256(hashRep) +} + +// GenesisBalances contains the information needed to generate a new ledger +type GenesisBalances struct { + Balances map[Address]Account + FeeSink Address + RewardsPool Address + Timestamp int64 +} + +// Balances returns the genesis account balances. +func (genesis Genesis) Balances() (GenesisBalances, error) { + genalloc := make(map[Address]Account) + for _, entry := range genesis.Allocation { + addr, err := DecodeAddress(entry.Address) + if err != nil { + return GenesisBalances{}, fmt.Errorf("cannot parse genesis addr %s: %w", entry.Address, err) + } + + _, present := genalloc[addr] + if present { + return GenesisBalances{}, fmt.Errorf("repeated allocation to %s", entry.Address) + } + + genalloc[addr] = entry.State + } + + feeSink, err := DecodeAddress(genesis.FeeSink) + if err != nil { + return GenesisBalances{}, fmt.Errorf("cannot parse fee sink addr %s: %w", genesis.FeeSink, err) + } + + rewardsPool, err := DecodeAddress(genesis.RewardsPool) + if err != nil { + return GenesisBalances{}, fmt.Errorf("cannot parse rewards pool addr %s: %w", genesis.RewardsPool, err) + } + + return MakeTimestampedGenesisBalances(genalloc, feeSink, rewardsPool, genesis.Timestamp), nil +} + +// MakeTimestampedGenesisBalances returns the information needed to bootstrap the ledger based on a given time +func MakeTimestampedGenesisBalances(balances map[Address]Account, feeSink, rewardsPool Address, timestamp int64) GenesisBalances { + return GenesisBalances{Balances: balances, FeeSink: feeSink, RewardsPool: rewardsPool, Timestamp: timestamp} +} diff --git a/types/genesis_test.go b/types/genesis_test.go new file mode 100644 index 00000000..e6537d7c --- /dev/null +++ b/types/genesis_test.go @@ -0,0 +1,155 @@ +package types + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "testing" + + "github.com/algorand/go-algorand-sdk/v2/encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeDecodeGenesis(t *testing.T) { + genesisStr, err := ioutil.ReadFile("test_resource/mainnet_genesis.json") + require.NoError(t, err) + + var genesis Genesis + err = json.Decode(genesisStr, &genesis) + require.NoError(t, err) + assert.Equal(t, base64.StdEncoding.EncodeToString(json.Encode(genesis)), base64.StdEncoding.EncodeToString(genesisStr)) + // hash value taken from https://developer.algorand.org/docs/get-details/algorand-networks/mainnet/#genesis-hash + expectedHash := "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=" + gh := genesis.Hash() + b := gh[:] + assert.Equal(t, base64.StdEncoding.EncodeToString(b), expectedHash) +} + +func TestGenesis_Balances(t *testing.T) { + containsErrorFunc := func(str string) assert.ErrorAssertionFunc { + return func(_ assert.TestingT, err error, i ...interface{}) bool { + require.ErrorContains(t, err, str) + return true + } + } + mustAddr := func(addr string) Address { + address, err := DecodeAddress(addr) + require.NoError(t, err) + return address + } + makeAddr := func(addr uint64) Address { + var address Address + address[0] = byte(addr) + return address + } + acctWith := func(algos uint64, addr string) GenesisAllocation { + return GenesisAllocation{ + _struct: struct{}{}, + Address: addr, + Comment: "", + State: Account{ + MicroAlgos: algos, + }, + } + } + goodAddr := makeAddr(100) + allocation1 := acctWith(1000, makeAddr(1).String()) + allocation2 := acctWith(2000, makeAddr(2).String()) + badAllocation := acctWith(1234, "El Toro Loco") + type fields struct { + Allocation []GenesisAllocation + FeeSink string + RewardsPool string + } + tests := []struct { + name string + fields fields + want GenesisBalances + wantErr assert.ErrorAssertionFunc + }{ + { + name: "basic test", + fields: fields{ + Allocation: []GenesisAllocation{allocation1}, + FeeSink: goodAddr.String(), + RewardsPool: goodAddr.String(), + }, + want: GenesisBalances{ + Balances: map[Address]Account{ + mustAddr(allocation1.Address): allocation1.State, + }, + FeeSink: goodAddr, + RewardsPool: goodAddr, + Timestamp: 0, + }, + wantErr: assert.NoError, + }, + { + name: "two test", + fields: fields{ + Allocation: []GenesisAllocation{allocation1, allocation2}, + FeeSink: goodAddr.String(), + RewardsPool: goodAddr.String(), + }, + want: GenesisBalances{ + Balances: map[Address]Account{ + mustAddr(allocation1.Address): allocation1.State, + mustAddr(allocation2.Address): allocation2.State, + }, + FeeSink: goodAddr, + RewardsPool: goodAddr, + Timestamp: 0, + }, + wantErr: assert.NoError, + }, + { + name: "bad fee sink", + fields: fields{ + Allocation: []GenesisAllocation{allocation1, allocation2}, + RewardsPool: goodAddr.String(), + }, + wantErr: containsErrorFunc("cannot parse fee sink addr"), + }, + { + name: "bad rewards pool", + fields: fields{ + Allocation: []GenesisAllocation{allocation1, allocation2}, + FeeSink: goodAddr.String(), + }, + wantErr: containsErrorFunc("cannot parse rewards pool addr"), + }, + { + name: "bad genesis addr", + fields: fields{ + Allocation: []GenesisAllocation{badAllocation}, + FeeSink: goodAddr.String(), + RewardsPool: goodAddr.String(), + }, + wantErr: containsErrorFunc("cannot parse genesis addr"), + }, + { + name: "repeat address", + fields: fields{ + Allocation: []GenesisAllocation{allocation1, allocation1}, + FeeSink: goodAddr.String(), + RewardsPool: goodAddr.String(), + }, + wantErr: containsErrorFunc("repeated allocation to"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + genesis := Genesis{ + Allocation: tt.fields.Allocation, + FeeSink: tt.fields.FeeSink, + RewardsPool: tt.fields.RewardsPool, + } + got, err := genesis.Balances() + if tt.wantErr(t, err, fmt.Sprintf("Balances()")) { + return + } + assert.Equalf(t, tt.want, got, "Balances()") + }) + } +} diff --git a/types/statedelta.go b/types/statedelta.go new file mode 100644 index 00000000..22a1b45f --- /dev/null +++ b/types/statedelta.go @@ -0,0 +1,327 @@ +package types + +// TealType is an enum of the types in a TEAL program: Bytes and Uint +type TealType uint64 + +const ( + // TealBytesType represents the type of a byte slice in a TEAL program + TealBytesType TealType = 1 + + // TealUintType represents the type of a uint in a TEAL program + TealUintType TealType = 2 +) + +// Status is the delegation status of an account's MicroAlgos +type Status byte + +const ( + // Offline indicates that the associated account receives rewards but does not participate in the consensus. + Offline Status = iota + // Online indicates that the associated account participates in the consensus and receive rewards. + Online + // NotParticipating indicates that the associated account neither participates in the consensus, nor receives rewards. + // Accounts that are marked as NotParticipating cannot change their status, but can receive and send Algos to other accounts. + // Two special accounts that are defined as NotParticipating are the incentive pool (also know as rewards pool) and the fee sink. + // These two accounts also have additional Algo transfer restrictions. + NotParticipating +) + +// TealValue contains type information and a value, representing a value in a +// TEAL program +type TealValue struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Type TealType `codec:"tt"` + Bytes string `codec:"tb"` + Uint uint64 `codec:"ui"` +} + +// TealKeyValue represents a key/value store for use in an application's +// LocalState or GlobalState +//msgp:allocbound TealKeyValue EncodedMaxKeyValueEntries +type TealKeyValue map[string]TealValue + +// StateSchemas is a thin wrapper around the LocalStateSchema and the +// GlobalStateSchema, since they are often needed together +type StateSchemas struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + LocalStateSchema StateSchema `codec:"lsch"` + GlobalStateSchema StateSchema `codec:"gsch"` +} + +// AppParams stores the global information associated with an application +type AppParams struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + ApprovalProgram []byte `codec:"approv,allocbound=config.MaxAvailableAppProgramLen"` + ClearStateProgram []byte `codec:"clearp,allocbound=config.MaxAvailableAppProgramLen"` + GlobalState TealKeyValue `codec:"gs"` + StateSchemas + ExtraProgramPages uint32 `codec:"epp"` +} + +// AppLocalState stores the LocalState associated with an application. It also +// stores a cached copy of the application's LocalStateSchema so that +// MinBalance requirements may be computed 1. without looking up the +// AppParams and 2. even if the application has been deleted +type AppLocalState struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Schema StateSchema `codec:"hsch"` + KeyValue TealKeyValue `codec:"tkv"` +} + +// AppLocalStateDelta tracks a changed AppLocalState, and whether it was deleted +type AppLocalStateDelta struct { + LocalState *AppLocalState + Deleted bool +} + +// AppParamsDelta tracks a changed AppParams, and whether it was deleted +type AppParamsDelta struct { + Params *AppParams + Deleted bool +} + +// AppResourceRecord represents AppParams and AppLocalState in deltas +type AppResourceRecord struct { + Aidx AppIndex + Addr Address + Params AppParamsDelta + State AppLocalStateDelta +} + +// AssetHolding describes an asset held by an account. +type AssetHolding struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Amount uint64 `codec:"a"` + Frozen bool `codec:"f"` +} + +// AssetHoldingDelta records a changed AssetHolding, and whether it was deleted +type AssetHoldingDelta struct { + Holding *AssetHolding + Deleted bool +} + +// AssetParamsDelta tracks a changed AssetParams, and whether it was deleted +type AssetParamsDelta struct { + Params *AssetParams + Deleted bool +} + +// AssetResourceRecord represents AssetParams and AssetHolding in deltas +type AssetResourceRecord struct { + Aidx AssetIndex + Addr Address + Params AssetParamsDelta + Holding AssetHoldingDelta +} + +// AccountAsset is used as a map key. +type AccountAsset struct { + Address Address + Asset AssetIndex +} + +// AccountApp is used as a map key. +type AccountApp struct { + Address Address + App AppIndex +} + +// A OneTimeSignatureVerifier is used to identify the holder of +// OneTimeSignatureSecrets and prove the authenticity of OneTimeSignatures +// against some OneTimeSignatureIdentifier. +type OneTimeSignatureVerifier [32]byte + +// VRFVerifier is a deprecated name for VrfPubkey +type VRFVerifier [32]byte + +// VotingData holds participation information +type VotingData struct { + VoteID OneTimeSignatureVerifier + SelectionID VRFVerifier + StateProofID Commitment + + VoteFirstValid Round + VoteLastValid Round + VoteKeyDilution uint64 +} + +// AccountBaseData contains base account info like balance, status and total number of resources +type AccountBaseData struct { + Status Status + MicroAlgos MicroAlgos + RewardsBase uint64 + RewardedMicroAlgos MicroAlgos + AuthAddr Address + + TotalAppSchema StateSchema // Totals across created globals, and opted in locals. + TotalExtraAppPages uint32 // Total number of extra pages across all created apps + TotalAppParams uint64 // Total number of apps this account has created + TotalAppLocalStates uint64 // Total number of apps this account is opted into. + TotalAssetParams uint64 // Total number of assets created by this account + TotalAssets uint64 // Total of asset creations and optins (i.e. number of holdings) + TotalBoxes uint64 // Total number of boxes associated to this account + TotalBoxBytes uint64 // Total bytes for this account's boxes. keys _and_ values count +} + +// AccountData provides users of the Balances interface per-account data (like basics.AccountData) +// but without any maps containing AppParams, AppLocalState, AssetHolding, or AssetParams. This +// ensures that transaction evaluation must retrieve and mutate account, asset, and application data +// separately, to better support on-disk and in-memory schemas that do not store them together. +type AccountData struct { + AccountBaseData + VotingData +} + +// BalanceRecord is similar to basics.BalanceRecord but with decoupled base and voting data +type BalanceRecord struct { + Addr Address + AccountData +} + +// AccountDeltas stores ordered accounts and allows fast lookup by address +// One key design aspect here was to ensure that we're able to access the written +// deltas in a deterministic order, while maintaining O(1) lookup. In order to +// do that, each of the arrays here is constructed as a pair of (slice, map). +// The map would point the address/address+creatable id onto the index of the +// element within the slice. +// If adding fields make sure to add them to the .reset() method to avoid dirty state +type AccountDeltas struct { + // Actual data. If an account is deleted, `Accts` contains the BalanceRecord + // with an empty `AccountData` and a populated `Addr`. + Accts []BalanceRecord + // cache for addr to deltas index resolution + acctsCache map[Address]int + + // AppResources deltas. If app params or local state is deleted, there is a nil value in AppResources.Params or AppResources.State and Deleted flag set + AppResources []AppResourceRecord + // caches for {addr, app id} to app params delta resolution + // not preallocated - use UpsertAppResource instead of inserting directly + appResourcesCache map[AccountApp]int + + AssetResources []AssetResourceRecord + // not preallocated - use UpsertAssertResource instead of inserting directly + assetResourcesCache map[AccountAsset]int +} + +// A KvValueDelta shows how the Data associated with a key in the kvstore has +// changed. However, OldData is elided during evaluation, and only filled in at +// the conclusion of a block during the called to roundCowState.deltas() +type KvValueDelta struct { + // Data stores the most recent value (nil == deleted) + Data []byte + + // OldData stores the previous vlaue (nil == didn't exist) + OldData []byte +} + +// IncludedTransactions defines the transactions included in a block, their index and last valid round. +type IncludedTransactions struct { + LastValid Round + Intra uint64 // the index of the transaction in the block +} + +// Txid is a hash used to uniquely identify individual transactions +type Txid Digest + +// A Txlease is a transaction (sender, lease) pair which uniquely specifies a +// transaction lease. +type Txlease struct { + Sender Address + Lease [32]byte +} + +// CreatableIndex represents either an AssetIndex or AppIndex, which come from +// the same namespace of indices as each other (both assets and apps are +// "creatables") +type CreatableIndex uint64 + +// CreatableType is an enum representing whether or not a given creatable is an +// application or an asset +type CreatableType uint64 + +// ModifiedCreatable defines the changes to a single single creatable state +type ModifiedCreatable struct { + // Type of the creatable: app or asset + Ctype CreatableType + + // Created if true, deleted if false + Created bool + + // creator of the app/asset + Creator Address + + // Keeps track of how many times this app/asset appears in + // accountUpdates.creatableDeltas + Ndeltas int +} + +// AlgoCount represents a total of algos of a certain class +// of accounts (split up by their Status value). +type AlgoCount struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + // Sum of algos of all accounts in this class. + Money MicroAlgos `codec:"mon"` + + // Total number of whole reward units in accounts. + RewardUnits uint64 `codec:"rwd"` +} + +// AccountTotals represents the totals of algos in the system +// grouped by different account status values. +type AccountTotals struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Online AlgoCount `codec:"online"` + Offline AlgoCount `codec:"offline"` + NotParticipating AlgoCount `codec:"notpart"` + + // Total number of algos received per reward unit since genesis + RewardsLevel uint64 `codec:"rwdlvl"` +} + +// LedgerStateDelta describes the delta between a given round to the previous round +// If adding a new field not explicitly allocated by PopulateStateDelta, make sure to reset +// it in .ReuseStateDelta to avoid dirty memory errors. +// If adding fields make sure to add them to the .Reset() method to avoid dirty state +type LedgerStateDelta struct { + // modified new accounts + Accts AccountDeltas + + // modified kv pairs (nil == delete) + // not preallocated use .AddKvMod to insert instead of direct assignment + KvMods map[string]KvValueDelta + + // new Txids for the txtail and TxnCounter, mapped to txn.LastValid + Txids map[Txid]IncludedTransactions + + // new txleases for the txtail mapped to expiration + // not pre-allocated so use .AddTxLease to insert instead of direct assignment + Txleases map[Txlease]Round + + // new creatables creator lookup table + // not pre-allocated so use .AddCreatable to insert instead of direct assignment + Creatables map[CreatableIndex]ModifiedCreatable + + // new block header; read-only + Hdr *BlockHeader + + // next round for which we expect a state proof. + // zero if no state proof is expected. + StateProofNext Round + + // previous block timestamp + PrevTimestamp int64 + + // initial hint for allocating data structures for StateDelta + initialHint int + + // The account totals reflecting the changes in this StateDelta object. + Totals AccountTotals +} diff --git a/types/test_resource/mainnet_genesis.json b/types/test_resource/mainnet_genesis.json new file mode 100644 index 00000000..987a290b --- /dev/null +++ b/types/test_resource/mainnet_genesis.json @@ -0,0 +1,946 @@ +{ + "alloc": [ + { + "addr": "737777777777777777777777777777777777777777777777777UFEJ2CI", + "comment": "RewardsPool", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA", + "comment": "FeeSink", + "state": { + "algo": 1000000, + "onl": 2 + } + }, + { + "addr": "ALGORANDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIN5DNAU", + "comment": "A BIT DOES E NOT BUT E STARTS EVERYTHING LIFE A MANY FORTUNE R BUILD SIMPLER BE THE STARTS PERSEVERES FAVORS A ENOUGH RIPROVANDO POSSIBLE JOURNEY VICTORIA HE BOLD U WITHOUT MEN A K OF BORDERS WHO HE E RACES TOMORROW BUT WHO SINGLE PURPOSE GEOGRAPHICAL PROVANDO A KNOW SUFFOCATES NOT SCIENCE STEP MATHEMATICS OF OR A BRIDGES WALLS TECHNOLOGY TODAY AND WITH AS ET MILES OF THOUSAND VITA SIMPLE TOO MUST AS NOT MADE NOT", + "state": { + "algo": 1000000, + "onl": 2 + } + }, + { + "addr": "XQJEJECPWUOXSKMIC5TCSARPVGHQJIIOKHO7WTKEPPLJMKG3D7VWWID66E", + "comment": "AlgorandCommunityAnnouncement", + "state": { + "algo": 10000000, + "onl": 2 + } + }, + { + "addr": "VCINCVUX2DBKQ6WP63NOGPEAQAYGHGSGQX7TSH4M5LI5NBPVAGIHJPMIPM", + "comment": "AuctionsMaster", + "state": { + "algo": 1000000000, + "onl": 2 + } + }, + { + "addr": "OGP6KK5KCMHT4GOEQXJ4LLNJ7D6P6IH7MV5WZ5EX4ZWACHP75ID5PPEE5E", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "AYBHAG2DAIOG26QEV35HKUBGWPMPOCCQ44MQEY32UOW3EXEMSZEIS37M2U", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "2XKK2L6HOBCYHGIGBS3N365FJKHS733QOX42HIYLSBARUIJHMGQZYAQDRY", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "ZBSPQQG7O5TR5MHPG3D5RS2TIFFD5NMOPR77VUKURMN6HV2BSN224ZHKGU", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "7NQED6NJ4NZU7B5HGGFU2ZEC2UZQYU2SA5S4QOE2EXBVAR4CNAHIXV2XYY", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "RX2ZKVJ43GNYDJNIOB6TIX26U7UEQFUQY46OMHX6CXLMMBHENJIH4YVLUQ", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "RHSKYCCZYYQ2BL6Z63626YUETJMLFGVVV47ED5D55EKIK4YFJ5DQT5CV4A", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "RJS6FDZ46ZZJIONLMMCKDJHYSJNHHAXNABMAVSGH23ULJSEAHZC6AQ6ALE", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "AZ2KKAHF2PJMEEUVN4E2ILMNJCSZLJJYVLBIA7HOY3BQ7AENOVVTXMGN3I", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "CGUKRKXNMEEOK7SJKOGXLRWEZESF24ELG3TAW6LUF43XRT2LX4OVQLU4BQ", + "comment": "", + "state": { + "algo": 300000000000000, + "onl": 2 + } + }, + { + "addr": "VVW6BVYHBG7MZQXKAR3OSPUZVAZ66JMQHOBMIBJG6YSPR7SLMNAPA7UWGY", + "comment": "", + "state": { + "algo": 250000000000000, + "onl": 2 + } + }, + { + "addr": "N5BGWISAJSYT7MVW2BDTTEHOXFQF4QQH4VKSMKJEOA4PHPYND43D6WWTIU", + "comment": "", + "state": { + "algo": 1740000000000000, + "onl": 2 + } + }, + { + "addr": "MKT3JAP2CEI5C4IX73U7QKRUF6JR7KPKE2YD6BLURFVPW6N7CYXVBSJPEQ", + "comment": "", + "state": { + "algo": 158000000000000, + "onl": 2 + } + }, + { + "addr": "GVCPSWDNSL54426YL76DZFVIZI5OIDC7WEYSJLBFFEQYPXM7LTGSDGC4SA", + "comment": "", + "state": { + "algo": 49998988000000, + "onl": 1, + "sel": "lZ9z6g0oSlis/8ZlEyOMiGfX0XDUcObfpJEg5KjU0OA=", + "vote": "Kk+5CcpHWIXSMO9GiAvnfe+eNSeRtpDb2telHb6I1EE=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "M7XKTBQXVQARLS7IVS6NVDHNLJFIAXR2CGGZTUDEKRIHRVLWL5TJFJOL5U", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "Z5gE/m2E/WSuaS5E8aYzO2DugTdSWQdc5W5BroCJdms=", + "vote": "QHHw03LnZQhKvjjIxVj3+qwgohOij2j3TBDMy7V9JMk=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "QFYWTHPNZBKKZ4XG2OWVNEX6ETBISD2VJZTCMODIZKT3QHQ4TIRJVEDVV4", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "NthIIUyiiRVnU/W13ajFFV4EhTvT5EZR/9N6ZRD/Z7U=", + "vote": "3KtiTLYvHJqa+qkGFj2RcZC77bz9yUYKxBZt8B24Z+c=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "DPOZQ6HRYLNNWVQL3I4XV4LMK5UZVROKGJBRIYIRNZUBMVHCU4DZWDBHYE", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "PBZ/agWgmwMdmWgt/W0NvdTN/XSTrVhPvRSMjmP5j90=", + "vote": "FDONnMcq1acmIBjJr3vz4kx4Q8ZRZ8oIH8xXRV5c4L8=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "42GALMKS3HMDB24ZPOR237WQ5QDHL5NIRC3KIA4PCKENJZAD5RP5QPBFO4", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "p7axjoy3Wn/clD7IKoTK2Zahc5ZU+Qkt2POVHKugQU4=", + "vote": "PItHHw+b01XplxRBFmZniqmdm+RyJFYd0fDz+OP4D6o=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "OXWIMTRZA5TVPABJF534EBBERJG367OLAB6VFN4RAW5P6CQEMXEX7VVDV4", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "RSOWYRM6/LD7MYxlZGvvF+WFGmBZg7UUutdkaWql0Xo=", + "vote": "sYSYFRL7AMJ61egushOYD5ABh9p06C4ZRV/OUSx7o3g=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "AICDUO6E46YBJRLM4DFJPVRVZGOFTRNPF7UPQXWEPPYRPVGIMQMLY5HLFM", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "0vxjPZqEreAhUt9PHJU2Eerb7gBhMU+PgyEXYLmbifg=", + "vote": "fuc0z/tpiZXBWARCJa4jPdmDvSmun4ShQLFiAxQkOFI=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "DYATVHCICZA7VVOWZN6OLFFSKUAZ64TZ7WZWCJQBFWL3JL4VBBV6R7Z6IE", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "KO2035CRpp1XmVPOTOF6ICWCw/0I6FgelKxdwPq+gMY=", + "vote": "rlcoayAuud0suR3bvvI0+psi/NzxvAJUFlp+I4ntzkM=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "6XJH2PJMAXWS4RGE6NBYIS3OZFOPU3LOHYC6MADBFUAALSWNFHMPJUWVSE", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "PgW1dncjs9chAVM89SB0FD4lIrygxrf+uqsAeZw8Qts=", + "vote": "pA4NJqjTAtHGGvZWET9kliq24Go5kEW8w7f1BGAWmKY=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "EYOZMULFFZZ5QDDMWQ64HKIMUPPNEL3WJMNGAFD43L52ZXTPESBEVJPEZU", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 1, + "sel": "sfebD2noAbrn1vblMmeCIeGB3BxLGKQDTG4sKSNibFs=", + "vote": "Cuz3REj26J+JhOpf91u6PO6MV5ov5b1K/ii1U1uPD/g=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "I3345FUQQ2GRBHFZQPLYQQX5HJMMRZMABCHRLWV6RCJYC6OO4MOLEUBEGU", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "MkH9KsdwiFgYtFFWFu48CeejEop1vsyGFG4/kqPIOFg=", + "vote": "RcntidhQqXQIvYjLFtc6HuL335rMnNX92roa2LcC+qQ=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "6LQH42A4QJ3Y27FGKJWERY3MD65SXM4QQCJJR2HRJYNB427IQ73YBI3YFY", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "nF3mu9Bu0Ad5MIrT31NgTxxrsZOXc4u1+WCvaPQTYEQ=", + "vote": "NaqWR/7FzOq/MiHb3adO6+J+kvnQKat8NSqEmoEkVfE=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "3V2MC7WJGAFU2EHWBHEETIMJVFJNAT4KKWVPOMJFJIM6ZPWEJRJ4POTXGI", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "3i4K8zdmnf1kxwgcNmI3x50iIwAxDmLMvoQEhjzhado=", + "vote": "wfJWa0kby76rqX2yvCD/aCfJdNt+qItylDPQiuAWFkQ=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "FTXSKED23VEXNW442T2JKNPPNUC2WKFNRWBVQTFMT7HYX365IVLZXYILAI", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "icuL7ehcGonAcJ02Zy4MIHqcT+Sp1R1UURNCYJQHmo4=", + "vote": "tmFcj3v7X5DDxKI1IDbGdhXh3a5f0Ab1ftltM7TgIDE=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "IAOW7PXLCDGLKMIQF26IXFF4THSQMU662MUU6W5KPOXHIVKHYFLYRWOUT4", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "zTn9rl/8Y2gokMdFyFP/pKg4eP02arkxlrBZIS94vPI=", + "vote": "a0pX68GgY7u8bd2Z3311+Mtc6yDnESZmi9k8zJ0oHzY=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "4NRNE5RIGC2UGOMGMDR6L5YMQUV3Q76TPOR7TDU3WEMJLMC6BSBEKPJ2SY", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "orSV2VHPY8m5ckEHGwK0r+SM9jq4BujAICXegAUAecI=", + "vote": "NJ9tisH+7+S29m/uMymFTD8X02/PKU0JUX1ghnLCzkw=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "E2EIMPLDISONNZLXONGMC33VBYOIBC2R7LVOS4SYIEZYJQK6PYSAPQL7LQ", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "XM2iW9wg9G5TyOfVu9kTS80LDIqcEPkJsgxaZll3SWA=", + "vote": "p/opFfDOsIomj5j7pAYU+G/CNUIwvD2XdEer6dhGquQ=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "APDO5T76FB57LNURPHTLAGLQOHUQZXYHH2ZKR4DPQRKK76FB4IAOBVBXHQ", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "5k2vclbUQBE6zBl45F3kGSv1PYhE2k9wZjxyxoPlnwA=", + "vote": "3dcLRSckm3wd9KB0FBRxub3meIgT6lMZnv5F08GJgEo=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "3KJTYHNHK37G2JDZJPV55IHBADU22TX2FPJZJH43MY64IFWKVNMP2F4JZE", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "o5e9VLqMdmJas5wRovfYFHgQ+Z6sQoATf3a6j0HeIXU=", + "vote": "rG7J8pPAW+Xtu5pqMIJOG9Hxdlyewtf9zPHEKR2Q6OE=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "IVKDCE6MS44YVGMQQFVXCDABW2HKULKIXMLDS2AEOIA6P2OGMVHVJ64MZI", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "XgUrwumD7oin/rG3NKwywBSsTETg/aWg9MjCDG61Ybg=", + "vote": "sBPEGGrEqcQMdT+iq2ududNxCa/1HcluvsosO1SkE/k=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "2WDM5XFF7ONWFANPE5PBMPJLVWOEN2BBRLSKJ37PQYW5WWIHEFT3FV6N5Y", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "Lze5dARJdb1+Gg6ui8ySIi+LAOM3P9dKiHKB9HpMM6A=", + "vote": "ys4FsqUNQiv+N0RFtr0Hh9OnzVcxXS6cRVD/XrLgW84=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "EOZWAIPQEI23ATBWQ5J57FUMRMXADS764XLMBTSOLVKPMK5MK5DBIS3PCY", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "jtmLcJhaAknJtA1cS5JPZil4SQ5SKh8P0w1fUw3X0CE=", + "vote": "pyEtTxJAas/j+zi/N13b/3LB4UoCar1gfcTESl0SI2I=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "REMF542E5ZFKS7SGSNHTYB255AUITEKHLAATWVPK3CY7TAFPT6GNNCHH6M", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "8ggWPvRpSkyrjxoh1SVS9PiSjff2azWtH0HFadwI9Ck=", + "vote": "Ej/dSkWbzRf09RAuWZfC4luRPNuqkLFCSGYXDcOtwic=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "T4UBSEAKK7JHT7RNLXVHDRW72KKFJITITR54J464CAGE5FGAZFI3SQH3TI", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "eIB8MKaG2lyJyM9spk+b/Ap/bkbo9bHfvF9f8T51OQk=", + "vote": "7xuBsE5mJaaRAdm5wnINVwm4SgPqKwJTAS1QBQV3sEc=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "YUDNQMOHAXC4B3BAMRMMQNFDFZ7GYO2HUTBIMNIP7YQ4BL57HZ5VM3AFYU", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "CSTCDvvtsJB0VYUcl3oRXyiJfhm3CtqvRIuFYZ69Z68=", + "vote": "uBK1TH4xKdWfv5nnnHkvYssI0tyhWRFZRLHgVt9TE1k=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "4SZTEUQIURTRT37FCI3TRMHSYT5IKLUPXUI7GWC5DZFXN2DGTATFJY5ABY", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "THGOlrqElX13xMqeLUPy6kooTbXjiyrUoZfVccnHrfI=", + "vote": "k4hde2Q3Zl++sQobo01U8heZd/X0GIX1nyqM8aI/hCY=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "UEDD34QFEMWRGYCBLKZIEHPKSTNBFSRMFBHRJPY3O2JPGKHQCXH4IY6XRI", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "jE+AUFvtp2NJsfNeUZeXdWt0X6I58YOgY+z/HB17GDs=", + "vote": "lmnYTjg1FhRNAR9TwVmOahVr5Z+7H1GO6McmvOZZRTQ=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "HHZQOGQKMQDLBEL3HXMDX7AGTNORYVZ4JFDWVSL5QLWMD3EXOIAHDI5L7M", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "Hajdvzem2rR2GjLmCG+98clHZFY5Etlp0n+x/gQTGj0=", + "vote": "2+Ie4MDWC6o/SfFSqev1A7UAkzvKRESI42b4NKS6Iw8=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "XRTBXPKH3DXDJ5OLQSYXOGX3DJ3U5NR6Y3LIVIWMK7TY33YW4I2NJZOTVE", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "5qe7rVoQfGdIUuDbhP2ABWivCoCstKbUsjdmYY76akA=", + "vote": "3J3O9DyJMWKvACubUK9QvmCiArtZR7yFHWG7k7+apdQ=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "JJFGCPCZPYRLOUYBZVC4F7GRPZ5CLB6BMTVRGNDP7GRGXL6GG4JEN7DL54", + "comment": "", + "state": { + "algo": 24000000000000, + "onl": 1, + "sel": "YoRFAcTiOgJcLudNScYstbaKJ8anrrHwQMZAffWMqYE=", + "vote": "VQFKlDdxRqqqPUQ/mVoF8xZS9BGxUtTnPUjYyKnOVRA=", + "voteKD": 10000, + "voteLst": 3000000 + } + }, + { + "addr": "4VNSA2GZVUD5ZNO62OVVNP4NEL2LIEE5N3MZEK4BKH62KGKRLVINFZYTZM", + "comment": "", + "state": { + "algo": 100000000000000, + "onl": 2 + } + }, + { + "addr": "IVCEEIH2Q32DZNRTS5XFVEFFAQGERNZHHQT6S4UPY7ORJMHIQDSTX7YM4E", + "comment": "", + "state": { + "algo": 408400000000000, + "onl": 2 + } + }, + { + "addr": "PLFHBIRGM3ZWGAMCXTREX2N537TWOMFIQXHFO2ZGQOEPZU473SYBVGVA5M", + "comment": "", + "state": { + "algo": 1011600000000000, + "onl": 2 + } + }, + { + "addr": "KF7X4ZABZUQU7IFMHSKLDKWCS4F3GZLOLJRDAK5KMEMDAGU32CX36CJQ5M", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "BTEESEYQMFLWZKULSKLNDELYJTOOQK6ZT4FBCW3TOZQ55NZYLOO6BRQ5K4", + "comment": "", + "state": { + "algo": 36199095000000, + "onl": 2 + } + }, + { + "addr": "E36JOZVSZZDXKSERASLAWQE4NU67HC7Q6YDOCG7P7IRRWCPSWXOI245DPA", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "I5Q6RRN44OZWYMX6YLWHBGEVPL7S3GBUCMHZCOOLJ245TONH7PERHJXE4A", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "2GYS272T3W2AP4N2VX5BFBASVNLWN44CNVZVKLWMMVPZPHVJ52SJPPFQ2I", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "D5LSV2UGT4JJNSLJ5XNIF52WP4IHRZN46ZGWH6F4QEF4L2FLDYS6I6R35Y", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "UWMSBIP2CGCGR3GYVUIOW3YOMWEN5A2WRTTBH6Y23KE3MOVFRHNXBP6IOE", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "OF3MKZZ3L5ZN7AZ46K7AXJUI4UWJI3WBRRVNTDKYVZUHZAOBXPVR3DHINE", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "2PPWE36YUMWUVIFTV2A6U4MLZLGROW4GHYIRVHMUCHDH6HCNVPUKPQ53NY", + "comment": "", + "state": { + "algo": 440343426000000, + "onl": 2 + } + }, + { + "addr": "JRGRGRW4HYBNAAHR7KQLLBAGRSPOYY6TRSINKYB3LI5S4AN247TANH5IQY", + "comment": "", + "state": { + "algo": 362684706000000, + "onl": 2 + } + }, + { + "addr": "D7YVVQJXJEFOZYUHJLIJBW3ATCAW46ML62VYRJ3SMGLOHMWYH4OS3KNHTU", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "PZJKH2ILW2YDZNUIYQVJZ2MANRSMK6LCHAFSAPYT6R3L3ZCWKYRDZXRVY4", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "3MODEFJVPGUZH3HDIQ6L2MO3WLJV3FK3XSWKFBHUGZDCHXQMUKD4B7XLMI", + "comment": "", + "state": { + "algo": 130000000000000, + "onl": 2 + } + }, + { + "addr": "WNSA5P6C5IIH2UJPQWJX6FRNPHXY7XZZHOWLSW5ZWHOEHBUW4AD2H6TZGM", + "comment": "", + "state": { + "algo": 130000000000000, + "onl": 2 + } + }, + { + "addr": "OO65J5AIFDS6255WL3AESTUGJD5SUV47RTUDOUGYHEIME327GX7K2BGC6U", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "DM6A24ZWHRZRM2HWXUHAUDSAACO7VKEZAOC2THWDXH4DX5L7LSO3VF2OPU", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "NTJJSFM75RADUOUGOBHZB7IJGO7NLVBWA66EYOOPU67H7LYIXVSPSI7BTA", + "comment": "", + "state": { + "algo": 18099548000000, + "onl": 2 + } + }, + { + "addr": "DAV2AWBBW4HBGIL2Z6AAAWDWRJPTOQD6BSKU2CFXZQCOBFEVFEJ632I2LY", + "comment": "", + "state": { + "algo": 1000000000000, + "onl": 2 + } + }, + { + "addr": "M5VIY6QPSMALVVPVG5LVH35NBMH6XJMXNWKWTARGGTEEQNQ3BHPQGYP5XU", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "WZZLVKMCXJG3ICVZSVOVAGCCN755VHJKZWVSVQ6JPSRQ2H2OSPOOZKW6DQ", + "comment": "", + "state": { + "algo": 45248869000000, + "onl": 2 + } + }, + { + "addr": "XEJLJUZRQOLBHHSOJJUE4IWI3EZOM44P646UDKHS4AV2JW7ZWBWNFGY6BU", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "OGIPDCRJJPNVZ6X6NBQHMTEVKJVF74QHZIXVLABMGUKZWNMEH7MNXZIJ7Q", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "G47R73USFN6FJJQTI3JMYQXO7F6H4LRPBCTTAD5EZWPWY2WCG64AVPCYG4", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "PQ5T65QB564NMIY6HXNYZXTFRSTESUEFIF2C26ZZKIZE6Q4R4XFP5UYYWI", + "comment": "", + "state": { + "algo": 5000000000000, + "onl": 2 + } + }, + { + "addr": "R6S7TRMZCHNQPKP2PGEEJ6WYUKMTURNMM527ZQXABTHFT5GBVMF6AZAL54", + "comment": "", + "state": { + "algo": 1000000000000, + "onl": 2 + } + }, + { + "addr": "36LZKCBDUR5EHJ74Q6UWWNADLVJOHGCPBBQ5UTUM3ILRTQLA6RYYU4PUWQ", + "comment": "", + "state": { + "algo": 5000000000000, + "onl": 2 + } + }, + { + "addr": "JRHPFMSJLU42V75NTGFRQIALCK6RHTEK26QKLWCH2AEEAFNAVEXWDTA5AM", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "64VZVS2LFZXWA5W3S657W36LWGP34B7XLMDIF4ROXBTPADD7SR5WNUUYJE", + "comment": "", + "state": { + "algo": 171945701000000, + "onl": 2 + } + }, + { + "addr": "TXDBSEZPFP2UB6BDNFCHCZBTPONIIQVZGABM4UBRHVAAPR5NE24QBL6H2A", + "comment": "", + "state": { + "algo": 60000000000000, + "onl": 2 + } + }, + { + "addr": "XI5TYT4XPWUHE4AMDDZCCU6M4AP4CAI4VTCMXXUNS46I36O7IYBQ7SL3D4", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "Y6ZPKPXF2QHF6ULYQXVHM7NPI3L76SP6QHJHK7XTNPHNXDEUTJPRKUZBNE", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "6LY2PGUJLCK4Q75JU4IX5VWVJVU22VGJBWPZOFP3752UEBIUBQRNGJWIEA", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "L7AGFNAFJ6Z2FYCX3LXE4ZSERM2VOJF4KPF7OUCMGK6GWFXXDNHZJBEC2E", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "RYXX5U2HMWGTPBG2UDLDT6OXDDRCK2YGL7LFAKYNBLRGZGYEJLRMGYLSVU", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "S263NYHFQWZYLINTBELLMIRMAJX6J5CUMHTECTGGVZUKUN2XY6ND2QBZVY", + "comment": "", + "state": { + "algo": 21647524000000, + "onl": 2 + } + }, + { + "addr": "AERTZIYYGK3Q364M6DXPKSRRNSQITWYEDGAHXC6QXFCF4GPSCCSISAGCBY", + "comment": "", + "state": { + "algo": 19306244000000, + "onl": 2 + } + }, + { + "addr": "34UYPXOJA6WRTWRNH5722LFDLWT23OM2ZZTCFQ62EHQI6MM3AJIAKOWDVQ", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "EDVGNQL6APUFTIGFZHASIEWGJRZNWGIKJE64B72V36IQM2SJPOAG2MJNQE", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + }, + { + "addr": "RKKLUIIGR75DFWGQOMJB5ZESPT7URDPC7QHGYKM4MAJ4OEL2J5WAQF6Z2Q", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "M4TNVJLDZZFAOH2M24BE7IU72KUX3P6M2D4JN4WZXW7WXH3C5QSHULJOU4", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "WQL6MQS5SPK3CR3XUPYMGOUSCUC5PNW5YQPLGEXGKVRK3KFKSAZ6JK4HXQ", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "36JTK4PKUBJGVCWKXZTAG6VLJRXWZXQVPQQSYODSN6WEGVHOWSVK6O54YU", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "YFOAYI4SNXJR2DBEZ3O6FJOFSEQHWD7TYROCNDWF6VLBGLNJMRRHDXXZUI", + "comment": "", + "state": { + "algo": 30000000000000, + "onl": 2 + } + }, + { + "addr": "XASOPHD3KK3NNI5IF2I7S7U55RGF22SG6OEICVRMCTMMGHT3IBOJG7QWBU", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "H2AUGBLVQFHHFLFEPJ6GGJ7PBQITEN2GE6T7JZCALBKNU7Q52AVJM5HOYU", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "GX3XLHSRMFTADVKJBBQBTZ6BKINW6ZO5JHXWGCWB4CPDNPDQ2PIYN4AVHQ", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "VBJBJ4VC3IHUTLVLWMBON36Y5MPAMPV4DNGW5FQ47GRLPT7JR5PQOUST2E", + "comment": "", + "state": { + "algo": 4524887000000, + "onl": 2 + } + }, + { + "addr": "7AQVTOMB5DJRSUM4LPLVF6PY3Y5EBDF4RZNDIWNW4Z63JYTAQCPQ62IZFE", + "comment": "", + "state": { + "algo": 50000000000000, + "onl": 2 + } + }, + { + "addr": "B4ZIHKD4VYLA4BAFEP7KUHZD7PNWXW4QLCHCNKWRENJ2LYVEOIYA3ZX6IA", + "comment": "", + "state": { + "algo": 40000000000000, + "onl": 2 + } + }, + { + "addr": "G5RGT3EENES7UVIQUHXMJ5APMOGSW6W6RBC534JC6U2TZA4JWC7U27RADE", + "comment": "", + "state": { + "algo": 10000000000000, + "onl": 2 + } + }, + { + "addr": "5AHJFDLAXVINK34IGSI3JA5OVRVMPCWLFEZ6TA4I7XUZ7I6M34Q56DUYIM", + "comment": "", + "state": { + "algo": 20000000000000, + "onl": 2 + } + } + ], + "fees": "Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA", + "id": "v1.0", + "network": "mainnet", + "proto": "https://github.com/algorandfoundation/specs/tree/5615adc36bad610c7f165fa2967f4ecfa75125f0", + "rwd": "737777777777777777777777777777777777777777777777777UFEJ2CI", + "timestamp": 1560211200 +} \ No newline at end of file