diff --git a/.gitignore b/.gitignore index 38b09afe..e279e86c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ coverage.html # Mac .DS_Store + +# Testing files +*.feature +temp diff --git a/.travis.yml b/.travis.yml index 2532a26a..fdce182a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,6 @@ install: - chmod +x ~/sdkupdate.sh script: - - make build - - ~/sdkupdate.sh --go + - set -e + - go test `go list ./... | grep -v test` + - test/docker/run_docker.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 0879f21b..d160e2d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 1.3.0 +# Added +- additional Algorand Smart Contracts (ASC) + - support for Dynamic Fee contract + - support for Limit Order contract + - support for Periodic Payment contract +- support for SuggestedParams +- support for RawBlock request +- Missing transaction types # 1.2.1 # Added - Added asset decimals field. diff --git a/README.md b/README.md index ed5a6586..e08c7688 100644 --- a/README.md +++ b/README.md @@ -406,15 +406,13 @@ func main() { // Get the suggested transaction parameters txParams, err := algodClient.SuggestedParams() - if err != nil { - fmt.Printf("error getting suggested tx params: %s\n", err) - return - } + if err != nil { + fmt.Printf("error getting suggested tx params: %s\n", err) + return + } // Make transaction - genID := txParams.GenesisID - genHash := txParams.GenesisHash - tx, err := transaction.MakePaymentTxn(fromAddr, toAddr, 1000, 200000, txParams.LastRound, (txParams.LastRound + 1000), nil, "", genID, genHash) + tx, err := future.MakePaymentTxn(fromAddr, toAddr, 1000, nil, "", txParams) if err != nil { fmt.Printf("Error creating transaction: %s\n", err) return @@ -453,6 +451,7 @@ import ( "github.com/algorand/go-algorand-sdk/crypto" "github.com/algorand/go-algorand-sdk/mnemonic" "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/types" ) func main() { @@ -470,9 +469,15 @@ func main() { const amount = 20000 const firstRound = 642715 const lastRound = firstRound + 1000 - tx, err := transaction.MakePaymentTxn( + params := types.SuggestedParams { + Fee: types.MicroAlgos(fee), + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: []byte("JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI="), + } + tx, err := future.MakePaymentTxn( account.Address.String(), "4MYUHDWHWXAKA5KA7U5PEN646VYUANBFXVJNONBK3TIMHEMWMD4UBOJBI4", - fee, amount, firstRound, lastRound, nil, "", "", []byte("JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI="), + amount, nil, "", params ) if err != nil { fmt.Printf("Error creating transaction: %s\n", err) @@ -560,17 +565,20 @@ if err != nil { panic("invalid multisig parameters") } fromAddr, _ := ma.Address() -txn, err := transaction.MakePaymentTxn( +params := types.SuggestedParams { + Fee: types.MicroAlgos(fee), // fee per byte, unless FlatFee is true + FlatFee: false, + FirstRoundValid: types.Round(100000), + LastRoundValid: types.Round(101000), + GenesisHash: []byte, // cannot be empty in practice +} +txn, err := future.MakePaymentTxn( fromAddr.String(), "INSERTTOADDRESHERE", - 10, // fee per byte 10000, // amount - 100000, // first valid round - 101000, // last valid round - nil, // note - "", // closeRemainderTo - "", // genesisID - []byte, // genesisHash (Cannot be empty in practice) + nil, // note + "", // closeRemainderTo + params ) txid, txBytes, err := crypto.SignMultisigTransaction(secretKey, ma, txn) if err != nil { @@ -651,13 +659,21 @@ func submitGroup() { const fee = 1000 const amount1 = 2000 var note []byte - const genesisID = "XYZ" // replace me + const genesisID = "XYZ" // replace me genesisHash := []byte("ABC") // replace me const firstRound1 = 710399 - tx1, err := transaction.MakePaymentTxnWithFlatFee( - address1, address2, fee, amount1, firstRound1, firstRound1+1000, - note, "", genesisID, genesisHash, + params := types.SuggestedParams { + Fee: types.MicroAlgos(fee), + FlatFee: true, + FirstRoundValid: types.Round(firstRound1), + LastRoundValid: types.Round(firstRound1+1000), + GenesisHash: genesisHash, + GenesisID: genesisID, + } + tx1, err := future.MakePaymentTxn( + address1, address2, amount1, + note, "", params ) if err != nil { fmt.Printf("Failed to create payment transaction: %v\n", err) @@ -665,10 +681,12 @@ func submitGroup() { } const firstRound2 = 710515 + params.FirstRoundValid = types.Round(firstRound2) + params.LastRoundValid = types.Round(firstRound2 + 1000) const amount2 = 1500 - tx2, err := transaction.MakePaymentTxnWithFlatFee( - address2, address3, fee, amount2, firstRound2, firstRound2+1000, - note, "", genesisID, genesisHash, + tx2, err := future.MakePaymentTxn( + address2, address3, amount2, + note, "", params ) if err != nil { fmt.Printf("Failed to create payment transaction: %v\n", err) @@ -757,13 +775,21 @@ func main() { const fee = 1000 const amount = 2000 var note []byte - const genesisID = "XYZ" // replace me + const genesisID = "XYZ" // replace me genesisHash := []byte("ABC") // replace me const firstRound = 710399 - tx, err := transaction.MakePaymentTxnWithFlatFee( - sender.String(), receiver, fee, amount, firstRound, firstRound+1000, - note, "", genesisID, genesisHash, + params := types.SuggestedParams { + Fee: types.MicroAlgos(fee), + FlatFee: true, + FirstRoundValid: types.Round(firstRound), + LastRoundValid: types.Round(firstRound+1000), + GenesisHash: genesisHash, + GenesisID: genesisID, + } + tx, err := future.MakePaymentTxn( + sender.String(), receiver, amount, + note, "", params ) txid, stx, err := crypto.SignLogicsigTransaction(lsig, tx) @@ -790,12 +816,12 @@ Asset creation: This allows a user to issue a new asset. The user can define the whether there is an account that can revoke assets, whether there is an account that can freeze user accounts, whether there is an account that can be considered the asset reserve, and whether there is an account that can change the other accounts. The creating user can also do things like specify a name for the asset. - + ```golang addr := "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" // the account issuing the transaction; the asset creator -fee := uint64(10) // the number of microAlgos per byte to pay as a transaction fee +fee := types.MicroAlgos(10) // the number of microAlgos per byte to pay as a transaction fee defaultFrozen := false // whether user accounts will need to be unfrozen before transacting -genesisHash := "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" // hash of the genesis block of the network to be used +genesisHash, _ := base64.StdEncoding.DecodeString("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") // hash of the genesis block of the network to be used totalIssuance := uint64(100) // total number of this asset in circulation decimals := uint64(1) // hint to GUIs for interpreting base unit reserve := addr // specified address is considered the asset reserve (it has no special privileges, this is only informational) @@ -805,16 +831,24 @@ manager := addr // specified address can change reserve, freeze, clawback, and m unitName := "tst" // used to display asset units to user assetName := "testcoin" // "friendly name" of asset genesisID := "" // like genesisHash this is used to specify network to be used -firstRound := uint64(322575) // first Algorand round on which this transaction is valid -lastRound := uint64(322575) // last Algorand round on which this transaction is valid +firstRound := types.Round(322575) // first Algorand round on which this transaction is valid +lastRound := types.Round(322575) // last Algorand round on which this transaction is valid note := nil // arbitrary data to be stored in the transaction; here, none is stored assetURL := "http://someurl" // optional string pointing to a URL relating to the asset assetMetadataHash := "thisIsSomeLength32HashCommitment" // optional hash commitment of some sort relating to the asset. 32 character length. +params := types.SuggestedParams { + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: genesisHash, + GenesisID: genesisID, +} + // signing and sending "txn" allows "addr" to create an asset -txn, err = MakeAssetCreateTxn(addr, fee, firstRound, lastRound, note, - genesisID, genesisHash, totalIssuance, decimals, defaultFrozen, manager, reserve, freeze, clawback, - unitName, assetName, assetURL, assetMetadataHash) +txn, err = MakeAssetCreateTxn(addr, note, params, + totalIssuance, decimals, defaultFrozen, manager, reserve, freeze, clawback, + unitName, assetName, assetURL, assetMetadataHash) ``` @@ -824,16 +858,16 @@ Supplying an empty address is the same as turning the associated feature off for is set to the empty address, it can never change again. For example, if an asset configuration transaction specifying `clawback=""` were issued, the associated asset could never be revoked from asset holders, and `clawback=""` would be true for all time. The `strictEmptyAddressChecking` argument can help guard against this, it causes -`MakeAssetConfigTxn` return error if any management address is set to empty. +`MakeAssetConfigTxn` return error if any management address is set to empty. ```golang addr := "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" -fee := uint64(10) -firstRound := uint64(322575) -lastRound := uint64(322575) +fee := types.MicroAlgos(10) +firstRound := types.Round(322575) +lastRound := types.Round(322575) note := nil genesisID := "" -genesisHash := "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" +genesisHash, _ := base64.StdEncoding.DecodeString("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") assetIndex := uint64(1234) reserve := addr freeze := addr @@ -841,10 +875,18 @@ clawback := addr manager := addr strictEmptyAddressChecking := true +params := types.SuggestedParams { + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: genesisHash, + GenesisID: genesisID, +} + // signing and sending "txn" will allow the asset manager to change: // asset manager, asset reserve, asset freeze manager, asset revocation manager -txn, err = MakeAssetConfigTxn(addr, fee, firstRound, lastRound, note, - genesisID, genesisHash, assetIndex, manager, reserve, freeze, clawback, strictEmptyAddressChecking) +txn, err = MakeAssetConfigTxn(addr, note, params, + assetIndex, manager, reserve, freeze, clawback, strictEmptyAddressChecking) ``` @@ -853,77 +895,105 @@ by the creator. ```golang addr := "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" -fee := uint64(10) -firstRound := uint64(322575) -lastRound := uint64(322575) +fee := types.MicroAlgos(10) +firstRound := types.Round(322575) +lastRound := types.Round(322575) note := nil genesisID := "" -genesisHash := "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" +genesisHash, _ := base64.StdEncoding.DecodeString("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") assetIndex := uint64(1234) +params := types.SuggestedParams { + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: genesisHash, + GenesisID: genesisID, +} // if all outstanding assets are held by the asset creator, // the asset creator can sign and issue "txn" to remove the asset from the ledger. -txn, err = MakeAssetDestroyTxn(addr, fee, firstRound, lastRound, note, genesisID, genesisHash, assetIndex) +txn, err = MakeAssetDestroyTxn(addr, note, params, assetIndex) ``` Begin accepting an asset: Before a user can begin transacting with an asset, the user must first issue an asset acceptance transaction. This is a special case of the asset transfer transaction, where the user sends 0 assets to themself. After issuing this transaction, -the user can begin transacting with the asset. Each new accepted asset increases the user's minimum balance. +the user can begin transacting with the asset. Each new accepted asset increases the user's minimum balance. ```golang addr := "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" -fee := uint64(10) -firstRound := uint64(322575) -lastRound := uint64(322575) +fee := types.MicroAlgos(10) +firstRound := types.Round(322575) +lastRound := types.Round(322575) note := nil genesisID := "" -genesisHash := "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" +genesisHash, _ := base64.StdEncoding.DecodeString("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") assetIndex := uint64(1234) +params := types.SuggestedParams { + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: genesisHash, + GenesisID: genesisID, +} // signing and sending "txn" allows sender to begin accepting asset specified by creator and index -txn, err = MakeAssetAcceptanceTxn(addr, fee, firstRound, lastRound, note, genesisID, genesisHash, assetIndex) +txn, err = MakeAssetAcceptanceTxn(addr, note, params, assetIndex) ``` Transfer an asset: This allows users to transact with assets, after they have issued asset acceptance transactions. The optional `closeRemainderTo` argument can be used to stop transacting with a particular asset. Note: A frozen account can always close -out to the asset creator. +out to the asset creator. ```golang addr := "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" sender := addr recipient := "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" closeRemainderTo := "" // supply an address to close remaining balance after transfer to supplied address -fee := uint64(10) -firstRound := uint64(322575) -lastRound := uint64(322575) +fee := types.MicroAlgos(10) +firstRound := types.Round(322575) +lastRound := types.Round(322575) note := nil genesisID := "" -genesisHash := "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" +genesisHash, _ := base64.StdEncoding.DecodeString("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") assetIndex := uint64(1234) amount := uint64(10) +params := types.SuggestedParams { + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: genesisHash, + GenesisID: genesisID, +} + // signing and sending "txn" will send "amount" assets from "sender" to "recipient" -txn, err = MakeAssetTransferTxn(sender, recipient, closeRemainderTo, amount, fee, firstRound, lastRound, note, - genesisID, genesisHash, assetIndex); +txn, err = MakeAssetTransferTxn(sender, recipient, amount, note, params, closeRemainderTo, assetIndex); ``` Revoke an asset: This allows an asset's revocation manager to transfer assets on behalf of another user. It will only work when issued by the asset's revocation manager. ```golang revocationManager := "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" // txn signed by this address -recipient := "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" // assets sent to this address +recipient := "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" // assets sent to this address revocationTarget := "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" // assets come from this address -fee := uint64(10) -firstRound := uint64(322575) -lastRound := uint64(322575) +fee := types.MicroAlgos(10) +firstRound := types.Round(322575) +lastRound := types.Round(322575) note := nil genesisID := "" -genesisHash := "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" +genesisHash, _ := base64.StdEncoding.DecodeString("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") assetIndex := uint64(1234) amount := uint64(10) +params := types.SuggestedParams { + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: lastRound, + GenesisHash: genesisHash, + GenesisID: genesisID, +} + // signing and sending "txn" will send "amount" assets from "revocationTarget" to "recipient", // if and only if sender == clawback manager for this asset -txn, err = MakeAssetRevocationTxn(revocationManager, recipient, revocationTarget, amount, fee, firstRound, lastRound, note, - genesisID, genesisHash, assetIndex); +txn, err = MakeAssetRevocationTxn(revocationManager, revocationTarget, amount, recipient, note, params, assetIndex); ``` \ No newline at end of file diff --git a/client/algod/algod.go b/client/algod/algod.go index 32134c10..fb6f0cc9 100644 --- a/client/algod/algod.go +++ b/client/algod/algod.go @@ -101,8 +101,7 @@ func mergeRawQueries(q1, q2 string) string { } // submitForm is a helper used for submitting (ex.) GETs and POSTs to the server -func (client Client) submitForm(response interface{}, path string, request interface{}, requestMethod string, encodeJSON bool, headers []*Header) error { - var err error +func (client Client) submitFormRaw(path string, request interface{}, requestMethod string, encodeJSON bool, headers []*Header) (resp *http.Response, err error) { queryURL := client.serverURL // Handle version prefix @@ -119,13 +118,13 @@ func (client Client) submitForm(response interface{}, path string, request inter if rawRequestPaths[path] { reqBytes, ok := request.([]byte) if !ok { - return fmt.Errorf("couldn't decode raw request as bytes") + return nil, fmt.Errorf("couldn't decode raw request as bytes") } body = bytes.NewBuffer(reqBytes) } else { v, err := query.Values(request) if err != nil { - return err + return nil, err } queryURL.RawQuery = mergeRawQueries(queryURL.RawQuery, v.Encode()) @@ -138,7 +137,7 @@ func (client Client) submitForm(response interface{}, path string, request inter req, err = http.NewRequest(requestMethod, queryURL.String(), body) if err != nil { - return err + return nil, err } // If we add another endpoint that does not require auth, we should add a @@ -156,19 +155,28 @@ func (client Client) submitForm(response interface{}, path string, request inter } httpClient := &http.Client{} - resp, err := httpClient.Do(req) + resp, err = httpClient.Do(req) if err != nil { - return err + return nil, err } - defer resp.Body.Close() - err = extractError(resp) + if err != nil { + resp.Body.Close() + return nil, err + } + return resp, nil +} + +func (client Client) submitForm(response interface{}, path string, request interface{}, requestMethod string, encodeJSON bool, headers []*Header) error { + resp, err := client.submitFormRaw(path, request, requestMethod, encodeJSON, headers) if err != nil { return err } + defer resp.Body.Close() + dec := json.NewDecoder(resp.Body) return dec.Decode(&response) } diff --git a/client/algod/models/models.go b/client/algod/models/models.go index f39b9dea..dccf2028 100644 --- a/client/algod/models/models.go +++ b/client/algod/models/models.go @@ -6,7 +6,9 @@ // No client should depend on any package in v1. package models -import "github.com/algorand/go-algorand-sdk/types" +import ( + "github.com/algorand/go-algorand-sdk/types" +) // NodeStatus contains the information about a node status // swagger:model NodeStatus @@ -45,6 +47,15 @@ type NodeStatus struct { // // required: true CatchupTime int64 `json:"catchupTime"` + + // HasSyncedSinceStartup indicates whether a round has completed since startup + // Required: true + HasSyncedSinceStartup bool `json:"hasSyncedSinceStartup"` + + // StoppedAtUnsupportedRound indicates that the node does not support the new rounds and has stopped making progress + // + // Required: true + StoppedAtUnsupportedRound bool `json:"stoppedAtUnsupportedRound"` } // TransactionID Description @@ -56,6 +67,37 @@ type TransactionID struct { TxID string `json:"txId"` } +// Participation Description +// swagger:model Participation +type Participation struct { // Round and Address fields are redundant if Participation embedded in Account. Exclude for now. + // ParticipationPK is the root participation public key (if any) currently registered for this round + // + // required: true + // swagger:strfmt byte + ParticipationPK []byte `json:"partpkb64"` + + // VRFPK is the selection public key (if any) currently registered for this round + // + // required: true + // swagger:strfmt byte + VRFPK []byte `json:"vrfpkb64"` + + // VoteFirst is the first round for which this participation is valid. + // + // required: true + VoteFirst uint64 `json:"votefst"` + + // VoteLast is the last round for which this participation is valid. + // + // required: true + VoteLast uint64 `json:"votelst"` + + // VoteKeyDilution is the number of subkeys in for each batch of participation keys. + // + // required: true + VoteKeyDilution uint64 `json:"votekd"` +} + // Account Description // swagger:model Account type Account struct { @@ -86,7 +128,7 @@ type Account struct { // required: true AmountWithoutPendingRewards uint64 `json:"amountwithoutpendingrewards"` - // Rewards indicates the total rewards of MicroAlgos the account has recieved + // Rewards indicates the total rewards of MicroAlgos the account has received, including pending rewards. // // required: true Rewards uint64 `json:"rewards"` @@ -99,6 +141,13 @@ type Account struct { // required: true Status string `json:"status"` + // Participation is the participation information currently associated with the account, if any. + // This field is optional and may not be set even if participation information is registered. + // In future REST API versions, this field may become required. + // + // required: false + Participation *Participation `json:"participation,omitempty"` + // AssetParams specifies the parameters of assets created by this account. // // required: false @@ -111,6 +160,20 @@ type Account struct { Assets map[uint64]AssetHolding `json:"assets,omitempty"` } +// Asset specifies both the unique identifier and the parameters for an asset +// swagger:model Asset +type Asset struct { + // AssetIndex is the unique asset identifier + // + // required: true + AssetIndex uint64 + + // AssetParams specifies the parameters of asset referred to by AssetIndex + // + // required: true + AssetParams AssetParams +} + // AssetParams specifies the parameters for an asset. // swagger:model AssetParams type AssetParams struct { @@ -145,26 +208,26 @@ type AssetParams struct { // as supplied by the creator. // // required: false - UnitName string `json:"unitname"` + UnitName string `json:"unitname,omitempty"` // AssetName specifies the name of this asset, // as supplied by the creator. // // required: false - AssetName string `json:"assetname"` + AssetName string `json:"assetname,omitempty"` // URL specifies a URL where more information about the asset can be // retrieved // // required: false - URL string `json:"url"` + URL string `json:"url,omitempty"` // MetadataHash specifies a commitment to some unspecified asset // metadata. The format of this metadata is up to the application. // // required: false // swagger:strfmt byte - MetadataHash []byte `json:"metadatahash"` + MetadataHash []byte `json:"metadatahash,omitempty"` // ManagerAddr specifies the address used to manage the keys of this // asset and to destroy it. @@ -213,11 +276,6 @@ type AssetHolding struct { Frozen bool `json:"frozen"` } -// Bytes is a byte array -// swagger:strfmt binary -type Bytes = []byte // note that we need to make this its own object to get the strfmt annotation to work properly. Otherwise swagger generates []uint8 instead of type binary -// ^ one day we should probably fork swagger, to avoid this workaround. - // Transaction contains all fields common to all transactions and serves as an envelope to all transactions // type // swagger:model Transaction @@ -255,12 +313,23 @@ type Transaction struct { // Note is a free form data // // required: false - Note Bytes `json:"noteb64,omitempty"` + // swagger:strfmt byte + Note []byte `json:"noteb64,omitempty"` + + // Lease enforces mutual exclusion of transactions. If this field is + // nonzero, then once the transaction is confirmed, it acquires the + // lease identified by the (Sender, Lease) pair of the transaction until + // the LastValid round passes. While this transaction possesses the + // lease, no other transaction specifying this lease can be confirmed. + // + // required: false + // swagger:strfmt byte + Lease []byte `json:"lease,omitempty"` // ConfirmedRound indicates the block number this transaction appeared in // // required: false - ConfirmedRound uint64 `json:"round,omitempty"` + ConfirmedRound uint64 `json:"round"` // TransactionResults contains information about the side effects of a transaction // @@ -278,13 +347,37 @@ type Transaction struct { // This is a list of all supported transactions. // To add another one, create a struct with XXXTransactionType and embed it here. // To prevent extraneous fields, all must have the "omitempty" tag. + + // Payment contains the additional fields for a payment transaction. + // + // required: false Payment *PaymentTransactionType `json:"payment,omitempty"` + // Keyreg contains the additional fields for a keyreg transaction. + // + // required: false + Keyreg *KeyregTransactionType `json:"keyreg,omitempty"` + + // AssetConfig contains the additional fields for an asset config transaction. + // + // required: false + AssetConfig *AssetConfigTransactionType `json:"curcfg,omitempty"` + + // AssetTransfer contains the additional fields for an asset transfer transaction. + // + // required: false + AssetTransfer *AssetTransferTransactionType `json:"curxfer,omitempty"` + + // AssetFreeze contains the additional fields for an asset freeze transaction. + // + // required: false + AssetFreeze *AssetFreezeTransactionType `json:"curfrz,omitempty"` + // FromRewards is the amount of pending rewards applied to the From // account as part of this transaction. // // required: false - FromRewards uint64 `json:"fromrewards,omitempty"` + FromRewards uint64 `json:"fromrewards"` // Genesis ID // @@ -294,16 +387,14 @@ type Transaction struct { // Genesis hash // // required: true - GenesisHash Bytes `json:"genesishashb64"` -} + // swagger:strfmt byte + GenesisHash []byte `json:"genesishashb64"` -// TransactionResults contains information about the side effects of a transaction -// swagger:model TransactionResults -type TransactionResults struct { - // CreatedAssetIndex indicates the asset index of an asset created by this txn + // Group // // required: false - CreatedAssetIndex uint64 `json:"createdasset,omitempty"` + // swagger:strfmt byte + Group []byte `json:"group,omitempty"` } // PaymentTransactionType contains the additional fields for a payment Transaction @@ -333,13 +424,115 @@ type PaymentTransactionType struct { // as part of this transaction. // // required: false - ToRewards uint64 `json:"torewards,omitempty"` + ToRewards uint64 `json:"torewards"` // CloseRewards is the amount of pending rewards applied to the CloseRemainderTo // account as part of this transaction. // // required: false - CloseRewards uint64 `json:"closerewards,omitempty"` + CloseRewards uint64 `json:"closerewards"` +} + +// KeyregTransactionType contains the additional fields for a keyreg Transaction +// swagger:model KeyregTransactionType +type KeyregTransactionType struct { + // VotePK is the participation public key used in key registration transactions + // + // required: false + // swagger:strfmt byte + VotePK []byte `json:"votekey"` + + // SelectionPK is the VRF public key used in key registration transactions + // + // required: false + // swagger:strfmt byte + SelectionPK []byte `json:"selkey"` + + // VoteFirst is the first round this participation key is valid + // + // required: false + VoteFirst uint64 `json:"votefst"` + + // VoteLast is the last round this participation key is valid + // + // required: false + VoteLast uint64 `json:"votelst"` + + // VoteKeyDilution is the dilution for the 2-level participation key + // + // required: false + VoteKeyDilution uint64 `json:"votekd"` +} + +// TransactionResults contains information about the side effects of a transaction +// swagger:model TransactionResults +type TransactionResults struct { + // CreatedAssetIndex indicates the asset index of an asset created by this txn + // + // required: false + CreatedAssetIndex uint64 `json:"createdasset,omitempty"` +} + +// AssetConfigTransactionType contains the additional fields for an asset config transaction +// swagger:model AssetConfigTransactionType +type AssetConfigTransactionType struct { + // AssetID is the asset being configured (or empty if creating) + // + // required: false + AssetID uint64 `json:"id"` + + // Params specifies the new asset parameters (or empty if deleting) + // + // required: false + Params AssetParams `json:"params"` +} + +// AssetTransferTransactionType contains the additional fields for an asset transfer transaction +// swagger:model AssetTransferTransactionType +type AssetTransferTransactionType struct { + // AssetID is the asset being configured (or empty if creating) + // + // required: true + AssetID uint64 `json:"id"` + + // Amount is the amount being transferred. + // + // required: true + Amount uint64 `json:"amt"` + + // Sender is the source account (if using clawback). + // + // required: false + Sender string `json:"snd"` + + // Receiver is the recipient account. + // + // required: true + Receiver string `json:"rcv"` + + // CloseTo is the destination for remaining funds (if closing). + // + // required: false + CloseTo string `json:"closeto"` +} + +// AssetFreezeTransactionType contains the additional fields for an asset freeze transaction +// swagger:model AssetFreezeTransactionType +type AssetFreezeTransactionType struct { + // AssetID is the asset being configured (or empty if creating) + // + // required: true + AssetID uint64 `json:"id"` + + // Account specifies the account where the asset is being frozen or thawed. + // + // required: true + Account string `json:"acct"` + + // NewFreezeStatus specifies the new freeze status. + // + // required: true + NewFreezeStatus bool `json:"freeze"` } // TransactionList contains a list of transactions @@ -351,6 +544,15 @@ type TransactionList struct { Transactions []Transaction `json:"transactions,omitempty"` } +// AssetList contains a list of assets +// swagger:model AssetList +type AssetList struct { + // AssetList is a list of assets + // + // required: true + Assets []Asset `json:"assets,omitempty"` +} + // TransactionFee contains the suggested fee // swagger:model TransactionFee type TransactionFee struct { @@ -383,7 +585,8 @@ type TransactionParams struct { // Genesis hash // // required: true - GenesisHash Bytes `json:"genesishashb64"` + // swagger:strfmt byte + GenesisHash []byte `json:"genesishashb64"` // LastRound indicates the last round seen // @@ -395,6 +598,27 @@ type TransactionParams struct { // // required: true ConsensusVersion string `json:"consensusVersion"` + + // The minimum transaction fee (not per byte) required for the + // txn to validate for the current network protocol. + // + // required: false + MinTxnFee uint64 `json:"minFee"` +} + +// RawResponse is fulfilled by responses that should not be decoded as msgpack +type RawResponse interface { + SetBytes([]byte) +} + +// RawBlock represents an encoded msgpack block +// swagger:model RawBlock +// swagger:strfmt byte +type RawBlock []byte + +// SetBytes fulfills the RawResponse interface on RawBlock +func (rb *RawBlock) SetBytes(b []byte) { + *rb = b } // Block contains a block information @@ -544,11 +768,13 @@ type PendingTransactions struct { // swagger:model Version type Version struct { // required: true + // returns a list of supported protocol versions ( i.e. v1, v2, etc. ) Versions []string `json:"versions"` // required: true GenesisID string `json:"genesis_id"` // required: true - GenesisHash Bytes `json:"genesis_hash_b64"` + // swagger:strfmt byte + GenesisHash []byte `json:"genesis_hash_b64"` // required: true Build BuildVersion `json:"build"` } diff --git a/client/algod/wrappers.go b/client/algod/wrappers.go index ea83fa60..b3c33b8b 100644 --- a/client/algod/wrappers.go +++ b/client/algod/wrappers.go @@ -2,7 +2,10 @@ package algod import ( "context" + "errors" "fmt" + "github.com/algorand/go-algorand-sdk/types" + "io" "io/ioutil" "net/http" @@ -139,6 +142,21 @@ func (client Client) SuggestedParams(headers ...*Header) (response models.Transa return } +// BuildSuggestedParams gets the suggested transaction parameters and +// builds a types.SuggestedParams to pass to transaction builders (see package future) +func (client Client) BuildSuggestedParams(headers ...*Header) (response types.SuggestedParams, err error) { + var httpResponse models.TransactionParams + err = client.get(&httpResponse, "/transactions/params", nil, headers) + response.FlatFee = false + response.Fee = types.MicroAlgos(httpResponse.Fee) + response.GenesisID = httpResponse.GenesisID + response.GenesisHash = httpResponse.GenesisHash + response.FirstRoundValid = types.Round(httpResponse.LastRound) + response.LastRoundValid = types.Round(httpResponse.LastRound + 1000) + response.ConsensusVersion = httpResponse.ConsensusVersion + return +} + // SendRawTransaction gets the bytes of a SignedTxn and broadcasts it to the network func (client Client) SendRawTransaction(stx []byte, headers ...*Header) (response models.TransactionID, err error) { err = client.post(&response, "/transactions", stx, headers) @@ -151,6 +169,35 @@ func (client Client) Block(round uint64, headers ...*Header) (response models.Bl return } +func responseReadAll(resp *http.Response, maxContentLength int64) (body []byte, err error) { + if resp.ContentLength > 0 { + // more efficient path if we know the ContentLength + if maxContentLength > 0 && resp.ContentLength > maxContentLength { + return nil, errors.New("Content too long") + } + body = make([]byte, resp.ContentLength) + _, err = io.ReadFull(resp.Body, body) + return + } + + return ioutil.ReadAll(resp.Body) +} + +// BlockRaw gets the raw block msgpack bytes for the given round +func (client Client) BlockRaw(round uint64, headers ...*Header) (blockbytes []byte, err error) { + var resp *http.Response + request := struct { + Raw string `url:"raw"` + }{Raw: "1"} + resp, err = client.submitFormRaw(fmt.Sprintf("/block/%d", round), request, "GET", false, headers) + if err != nil { + return + } + defer resp.Body.Close() + // Current blocks are about 1MB. 10MB should be a safe backstop. + return responseReadAll(resp, 10000000) +} + func (client Client) doGetWithQuery(ctx context.Context, path string, queryArgs map[string]string) (result string, err error) { queryURL := client.serverURL queryURL.Path = path diff --git a/crypto/crypto.go b/crypto/crypto.go index c057a326..c711b7f2 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -66,7 +66,7 @@ func rawTransactionBytesToSign(tx types.Transaction) []byte { return bytes.Join(msgParts, nil) } -// txID computes a transaction id from raw transaction bytes +// txID computes a transaction id base32 string from raw transaction bytes func txIDFromRawTxnBytesToSign(toBeSigned []byte) (txid string) { txidBytes := sha512.Sum512_256(toBeSigned) txid = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(txidBytes[:]) @@ -75,7 +75,22 @@ func txIDFromRawTxnBytesToSign(toBeSigned []byte) (txid string) { // txIDFromTransaction is a convenience function for generating txID from txn func txIDFromTransaction(tx types.Transaction) (txid string) { - txid = txIDFromRawTxnBytesToSign(rawTransactionBytesToSign(tx)) + txidBytes := TransactionID(tx) + txid = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(txidBytes[:]) + return +} + +// TransactionID is the unique identifier for a Transaction in progress +func TransactionID(tx types.Transaction) (txid []byte) { + toBeSigned := rawTransactionBytesToSign(tx) + txid32 := sha512.Sum512_256(toBeSigned) + txid = txid32[:] + return +} + +// TransactionIDString is a base32 representation of a TransactionID +func TransactionIDString(tx types.Transaction) (txid string) { + txid = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(TransactionID(tx)) return } diff --git a/examples/gen-addresses/main.go b/examples/gen-addresses/main.go index 50ee9c14..da18a47c 100644 --- a/examples/gen-addresses/main.go +++ b/examples/gen-addresses/main.go @@ -7,7 +7,7 @@ import ( "github.com/algorand/go-algorand-sdk/client/algod" "github.com/algorand/go-algorand-sdk/client/kmd" "github.com/algorand/go-algorand-sdk/crypto" - "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/future" "github.com/algorand/go-algorand-sdk/types" ) @@ -106,16 +106,13 @@ func main() { privateKey := resp4.PrivateKey // Get the suggested transaction parameters - txParams, err := algodClient.SuggestedParams() + txParams, err := algodClient.BuildSuggestedParams() if err != nil { fmt.Printf("error getting suggested tx params: %s\n", err) return } - // Sign a sample transaction using this library, *not* kmd - genID := txParams.GenesisID - genHash := txParams.GenesisHash - tx, err := transaction.MakePaymentTxn(addresses[0], addresses[1], 1, 100, 300, 400, nil, "", genID, genHash) + tx, err := future.MakePaymentTxn(addresses[0], addresses[1], 100, nil, "", txParams) if err != nil { fmt.Printf("Error creating transaction: %s\n", err) return diff --git a/future/transaction.go b/future/transaction.go new file mode 100644 index 00000000..efb8945e --- /dev/null +++ b/future/transaction.go @@ -0,0 +1,530 @@ +package future + +import ( + "encoding/base64" + "fmt" + "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/types" +) + +// MinTxnFee is v5 consensus params, in microAlgos +const MinTxnFee = transaction.MinTxnFee + +// MakePaymentTxn constructs a payment transaction using the passed parameters. +// `from` and `to` addresses should be checksummed, human-readable addresses +// fee is fee per byte as received from algod SuggestedFee API call +func MakePaymentTxn(from, to string, amount uint64, note []byte, closeRemainderTo string, params types.SuggestedParams) (types.Transaction, error) { + // Decode from address + fromAddr, err := types.DecodeAddress(from) + if err != nil { + return types.Transaction{}, err + } + + // Decode to address + toAddr, err := types.DecodeAddress(to) + if err != nil { + return types.Transaction{}, err + } + + // Decode the CloseRemainderTo address, if present + var closeRemainderToAddr types.Address + if closeRemainderTo != "" { + closeRemainderToAddr, err = types.DecodeAddress(closeRemainderTo) + if err != nil { + return types.Transaction{}, err + } + } + + if len(params.GenesisHash) == 0 { + return types.Transaction{}, fmt.Errorf("payment transaction must contain a genesisHash") + } + + var gh types.Digest + copy(gh[:], params.GenesisHash) + + // Build the transaction + tx := types.Transaction{ + Type: types.PaymentTx, + Header: types.Header{ + Sender: fromAddr, + Fee: params.Fee, + FirstValid: params.FirstRoundValid, + LastValid: params.LastRoundValid, + Note: note, + GenesisID: params.GenesisID, + GenesisHash: gh, + }, + PaymentTxnFields: types.PaymentTxnFields{ + Receiver: toAddr, + Amount: types.MicroAlgos(amount), + CloseRemainderTo: closeRemainderToAddr, + }, + } + + // Update fee + if !params.FlatFee { + eSize, err := transaction.EstimateSize(tx) + if err != nil { + return types.Transaction{}, err + } + tx.Fee = types.MicroAlgos(eSize * uint64(params.Fee)) + } + + if tx.Fee < MinTxnFee { + tx.Fee = MinTxnFee + } + + return tx, nil +} + +// MakeKeyRegTxn constructs a keyreg transaction using the passed parameters. +// - account is a checksummed, human-readable address for which we register the given participation key. +// - note is a byte array +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// KeyReg parameters: +// - votePK is a base64-encoded string corresponding to the root participation public key +// - selectionKey is a base64-encoded string corresponding to the vrf public key +// - voteFirst is the first round this participation key is valid +// - voteLast is the last round this participation key is valid +// - voteKeyDilution is the dilution for the 2-level participation key +func MakeKeyRegTxn(account string, note []byte, params types.SuggestedParams, voteKey, selectionKey string, voteFirst, voteLast, voteKeyDilution uint64) (types.Transaction, error) { + // Decode account address + accountAddr, err := types.DecodeAddress(account) + if err != nil { + return types.Transaction{}, err + } + + if len(params.GenesisHash) == 0 { + return types.Transaction{}, fmt.Errorf("key registration transaction must contain a genesisHash") + } + + var gh types.Digest + copy(gh[:], params.GenesisHash) + + votePKBytes, err := byte32FromBase64(voteKey) + if err != nil { + return types.Transaction{}, err + } + + selectionPKBytes, err := byte32FromBase64(selectionKey) + if err != nil { + return types.Transaction{}, err + } + + tx := types.Transaction{ + Type: types.KeyRegistrationTx, + Header: types.Header{ + Sender: accountAddr, + Fee: params.Fee, + FirstValid: params.FirstRoundValid, + LastValid: params.LastRoundValid, + Note: note, + GenesisHash: gh, + GenesisID: params.GenesisID, + }, + KeyregTxnFields: types.KeyregTxnFields{ + VotePK: types.VotePK(votePKBytes), + SelectionPK: types.VRFPK(selectionPKBytes), + VoteFirst: types.Round(voteFirst), + VoteLast: types.Round(voteLast), + VoteKeyDilution: voteKeyDilution, + }, + } + + if !params.FlatFee { + // Update fee + eSize, err := transaction.EstimateSize(tx) + if err != nil { + return types.Transaction{}, err + } + tx.Fee = types.MicroAlgos(eSize * uint64(params.Fee)) + } + + if tx.Fee < MinTxnFee { + tx.Fee = MinTxnFee + } + + return tx, nil +} + +// MakeAssetCreateTxn constructs an asset creation transaction using the passed parameters. +// - account is a checksummed, human-readable address which will send the transaction. +// - note is a byte array +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// Asset creation parameters: +// - see asset.go +func MakeAssetCreateTxn(account string, note []byte, params types.SuggestedParams, total uint64, decimals uint32, defaultFrozen bool, manager, reserve, freeze, clawback string, unitName, assetName, url, metadataHash string) (types.Transaction, error) { + var tx types.Transaction + var err error + + if decimals > types.AssetMaxNumberOfDecimals { + return tx, fmt.Errorf("cannot create an asset with number of decimals %d (more than maximum %d)", decimals, types.AssetMaxNumberOfDecimals) + } + + tx.Type = types.AssetConfigTx + tx.AssetParams = types.AssetParams{ + Total: total, + Decimals: decimals, + DefaultFrozen: defaultFrozen, + UnitName: unitName, + AssetName: assetName, + URL: url, + } + + if manager != "" { + tx.AssetParams.Manager, err = types.DecodeAddress(manager) + if err != nil { + return tx, err + } + } + if reserve != "" { + tx.AssetParams.Reserve, err = types.DecodeAddress(reserve) + if err != nil { + return tx, err + } + } + if freeze != "" { + tx.AssetParams.Freeze, err = types.DecodeAddress(freeze) + if err != nil { + return tx, err + } + } + if clawback != "" { + tx.AssetParams.Clawback, err = types.DecodeAddress(clawback) + if err != nil { + return tx, err + } + } + + if len(assetName) > types.AssetNameMaxLen { + return tx, fmt.Errorf("asset name too long: %d > %d", len(assetName), types.AssetNameMaxLen) + } + tx.AssetParams.AssetName = assetName + + if len(url) > types.AssetURLMaxLen { + return tx, fmt.Errorf("asset url too long: %d > %d", len(url), types.AssetURLMaxLen) + } + tx.AssetParams.URL = url + + if len(unitName) > types.AssetUnitNameMaxLen { + return tx, fmt.Errorf("asset unit name too long: %d > %d", len(unitName), types.AssetUnitNameMaxLen) + } + tx.AssetParams.UnitName = unitName + + if len(metadataHash) > types.AssetMetadataHashLen { + return tx, fmt.Errorf("asset metadata hash '%s' too long: %d > %d)", metadataHash, len(metadataHash), types.AssetMetadataHashLen) + } + copy(tx.AssetParams.MetadataHash[:], []byte(metadataHash)) + + if len(params.GenesisHash) == 0 { + return types.Transaction{}, fmt.Errorf("asset transaction must contain a genesisHash") + } + var gh types.Digest + copy(gh[:], params.GenesisHash) + + // Fill in header + accountAddr, err := types.DecodeAddress(account) + if err != nil { + return types.Transaction{}, err + } + + tx.Header = types.Header{ + Sender: accountAddr, + Fee: params.Fee, + FirstValid: params.FirstRoundValid, + LastValid: params.LastRoundValid, + GenesisHash: gh, + GenesisID: params.GenesisID, + Note: note, + } + + // Update fee + if !params.FlatFee { + eSize, err := transaction.EstimateSize(tx) + if err != nil { + return types.Transaction{}, err + } + tx.Fee = types.MicroAlgos(eSize * uint64(params.Fee)) + } + + if tx.Fee < MinTxnFee { + tx.Fee = MinTxnFee + } + + return tx, nil +} + +// MakeAssetConfigTxn creates a tx template for changing the +// key configuration of an existing asset. +// Important notes - +// * Every asset config transaction is a fresh one. No parameters will be inherited from the current config. +// * Once an address is set to to the empty string, IT CAN NEVER BE CHANGED AGAIN. For example, if you want to keep +// The current manager, you must specify its address again. +// Parameters - +// - account is a checksummed, human-readable address that will send the transaction +// - note is an arbitrary byte array +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// - index is the asset index id +// - for newManager, newReserve, newFreeze, newClawback see asset.go +// - strictEmptyAddressChecking: if true, disallow empty admin accounts from being set (preventing accidental disable of admin features) +func MakeAssetConfigTxn(account string, note []byte, params types.SuggestedParams, index uint64, newManager, newReserve, newFreeze, newClawback string, strictEmptyAddressChecking bool) (types.Transaction, error) { + var tx types.Transaction + + if strictEmptyAddressChecking && (newManager == "" || newReserve == "" || newFreeze == "" || newClawback == "") { + return tx, fmt.Errorf("strict empty address checking requested but empty address supplied to one or more manager addresses") + } + + tx.Type = types.AssetConfigTx + + accountAddr, err := types.DecodeAddress(account) + if err != nil { + return tx, err + } + + if len(params.GenesisHash) == 0 { + return types.Transaction{}, fmt.Errorf("asset transaction must contain a genesisHash") + } + var gh types.Digest + copy(gh[:], params.GenesisHash) + + tx.Header = types.Header{ + Sender: accountAddr, + Fee: params.Fee, + FirstValid: params.FirstRoundValid, + LastValid: params.LastRoundValid, + GenesisHash: gh, + GenesisID: params.GenesisID, + Note: note, + } + + tx.ConfigAsset = types.AssetIndex(index) + + if newManager != "" { + tx.Type = types.AssetConfigTx + tx.AssetParams.Manager, err = types.DecodeAddress(newManager) + if err != nil { + return tx, err + } + } + + if newReserve != "" { + tx.AssetParams.Reserve, err = types.DecodeAddress(newReserve) + if err != nil { + return tx, err + } + } + + if newFreeze != "" { + tx.AssetParams.Freeze, err = types.DecodeAddress(newFreeze) + if err != nil { + return tx, err + } + } + + if newClawback != "" { + tx.AssetParams.Clawback, err = types.DecodeAddress(newClawback) + if err != nil { + return tx, err + } + } + + if !params.FlatFee { + // Update fee + eSize, err := transaction.EstimateSize(tx) + if err != nil { + return types.Transaction{}, err + } + tx.Fee = types.MicroAlgos(eSize * uint64(params.Fee)) + } + + if tx.Fee < MinTxnFee { + tx.Fee = MinTxnFee + } + + return tx, nil +} + +// transferAssetBuilder is a helper that builds asset transfer transactions: +// either a normal asset transfer, or an asset revocation +func transferAssetBuilder(account, recipient string, amount uint64, note []byte, params types.SuggestedParams, index uint64, closeAssetsTo, revocationTarget string) (types.Transaction, error) { + var tx types.Transaction + tx.Type = types.AssetTransferTx + + accountAddr, err := types.DecodeAddress(account) + if err != nil { + return tx, err + } + + if len(params.GenesisHash) == 0 { + return types.Transaction{}, fmt.Errorf("asset transaction must contain a genesisHash") + } + var gh types.Digest + copy(gh[:], params.GenesisHash) + + tx.Header = types.Header{ + Sender: accountAddr, + Fee: params.Fee, + FirstValid: params.FirstRoundValid, + LastValid: params.LastRoundValid, + GenesisHash: gh, + GenesisID: params.GenesisID, + Note: note, + } + + tx.XferAsset = types.AssetIndex(index) + + recipientAddr, err := types.DecodeAddress(recipient) + if err != nil { + return tx, err + } + tx.AssetReceiver = recipientAddr + + if closeAssetsTo != "" { + closeToAddr, err := types.DecodeAddress(closeAssetsTo) + if err != nil { + return tx, err + } + tx.AssetCloseTo = closeToAddr + } + + if revocationTarget != "" { + revokedAddr, err := types.DecodeAddress(revocationTarget) + if err != nil { + return tx, err + } + tx.AssetSender = revokedAddr + } + + tx.AssetAmount = amount + + // Update fee + eSize, err := transaction.EstimateSize(tx) + if err != nil { + return types.Transaction{}, err + } + tx.Fee = types.MicroAlgos(eSize * uint64(params.Fee)) + + if tx.Fee < MinTxnFee { + tx.Fee = MinTxnFee + } + + return tx, nil +} + +// MakeAssetTransferTxn creates a tx for sending some asset from an asset holder to another user +// the recipient address must have previously issued an asset acceptance transaction for this asset +// - account is a checksummed, human-readable address that will send the transaction and assets +// - recipient is a checksummed, human-readable address what will receive the assets +// - amount is the number of assets to send +// - note is an arbitrary byte array +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// - closeAssetsTo is a checksummed, human-readable address that behaves as a close-to address for the asset transaction; the remaining assets not sent to recipient will be sent to closeAssetsTo. Leave blank for no close-to behavior. +// - index is the asset index +func MakeAssetTransferTxn(account, recipient string, amount uint64, note []byte, params types.SuggestedParams, closeAssetsTo string, index uint64) (types.Transaction, error) { + revocationTarget := "" // no asset revocation, this is normal asset transfer + return transferAssetBuilder(account, recipient, amount, note, params, index, closeAssetsTo, revocationTarget) +} + +// MakeAssetAcceptanceTxn creates a tx for marking an account as willing to accept the given asset +// - account is a checksummed, human-readable address that will send the transaction and begin accepting the asset +// - note is an arbitrary byte array +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// - index is the asset index +func MakeAssetAcceptanceTxn(account string, note []byte, params types.SuggestedParams, index uint64) (types.Transaction, error) { + return MakeAssetTransferTxn(account, account, 0, note, params, "", index) +} + +// MakeAssetRevocationTxn creates a tx for revoking an asset from an account and sending it to another +// - account is a checksummed, human-readable address; it must be the revocation manager / clawback address from the asset's parameters +// - target is a checksummed, human-readable address; it is the account whose assets will be revoked +// - recipient is a checksummed, human-readable address; it will receive the revoked assets +// - amount defines the number of assets to clawback +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// - index is the asset index +func MakeAssetRevocationTxn(account, target string, amount uint64, recipient string, note []byte, params types.SuggestedParams, index uint64) (types.Transaction, error) { + closeAssetsTo := "" // no close-out, this is an asset revocation + return transferAssetBuilder(account, recipient, amount, note, params, index, closeAssetsTo, target) +} + +// MakeAssetDestroyTxn creates a tx template for destroying an asset, removing it from the record. +// All outstanding asset amount must be held by the creator, and this transaction must be issued by the asset manager. +// - account is a checksummed, human-readable address that will send the transaction; it also must be the asset manager +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// - index is the asset index +func MakeAssetDestroyTxn(account string, note []byte, params types.SuggestedParams, index uint64) (types.Transaction, error) { + // an asset destroy transaction is just a configuration transaction with AssetParams zeroed + return MakeAssetConfigTxn(account, note, params, index, "", "", "", "", false) +} + +// MakeAssetFreezeTxn constructs a transaction that freezes or unfreezes an account's asset holdings +// It must be issued by the freeze address for the asset +// - account is a checksummed, human-readable address which will send the transaction. +// - note is an optional arbitrary byte array +// - params is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +// - assetIndex is the index for tracking the asset +// - target is the account to be frozen or unfrozen +// - newFreezeSetting is the new state of the target account +func MakeAssetFreezeTxn(account string, note []byte, params types.SuggestedParams, assetIndex uint64, target string, newFreezeSetting bool) (types.Transaction, error) { + var tx types.Transaction + + tx.Type = types.AssetFreezeTx + + accountAddr, err := types.DecodeAddress(account) + if err != nil { + return tx, err + } + + if len(params.GenesisHash) == 0 { + return types.Transaction{}, fmt.Errorf("asset transaction must contain a genesisHash") + } + var gh types.Digest + copy(gh[:], params.GenesisHash) + + tx.Header = types.Header{ + Sender: accountAddr, + Fee: params.Fee, + FirstValid: params.FirstRoundValid, + LastValid: params.LastRoundValid, + GenesisHash: gh, + GenesisID: params.GenesisID, + Note: note, + } + + tx.FreezeAsset = types.AssetIndex(assetIndex) + + tx.FreezeAccount, err = types.DecodeAddress(target) + if err != nil { + return tx, err + } + + tx.AssetFrozen = newFreezeSetting + + if !params.FlatFee { + // Update fee + eSize, err := transaction.EstimateSize(tx) + if err != nil { + return types.Transaction{}, err + } + tx.Fee = types.MicroAlgos(eSize * uint64(params.Fee)) + } + + if tx.Fee < MinTxnFee { + tx.Fee = MinTxnFee + } + + return tx, nil +} + +// byte32FromBase64 decodes the input base64 string and outputs a +// 32 byte array, erroring if the input is the wrong length. +func byte32FromBase64(in string) (out [32]byte, err error) { + slice, err := base64.StdEncoding.DecodeString(in) + if err != nil { + return + } + if len(slice) != 32 { + return out, fmt.Errorf("Input is not 32 bytes") + } + copy(out[:], slice) + return +} diff --git a/future/transaction_test.go b/future/transaction_test.go new file mode 100644 index 00000000..fa802914 --- /dev/null +++ b/future/transaction_test.go @@ -0,0 +1,729 @@ +package future + +import ( + "encoding/base64" + "github.com/algorand/go-algorand-sdk/crypto" + "github.com/algorand/go-algorand-sdk/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/mnemonic" + "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/types" + "github.com/stretchr/testify/require" + "testing" +) + +func byteFromBase64(s string) []byte { + b, _ := base64.StdEncoding.DecodeString(s) + return b +} + +func byte32ArrayFromBase64(s string) (out [32]byte) { + slice := byteFromBase64(s) + if len(slice) != 32 { + panic("wrong length: input slice not 32 bytes") + } + copy(out[:], slice) + return +} + +func TestMakePaymentTxn(t *testing.T) { + const fromAddress = "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" + const toAddress = "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI" + const referenceTxID = "5FJDJD5LMZC3EHUYYJNH5I23U4X6H2KXABNDGPIL557ZMJ33GZHQ" + const mn = "advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor" + const golden = "gqNzaWfEQPhUAZ3xkDDcc8FvOVo6UinzmKBCqs0woYSfodlmBMfQvGbeUx3Srxy3dyJDzv7rLm26BRv9FnL2/AuT7NYfiAWjdHhui6NhbXTNA+ilY2xvc2XEIEDpNJKIJWTLzpxZpptnVCaJ6aHDoqnqW2Wm6KRCH/xXo2ZlZc0EmKJmds0wsqNnZW6sZGV2bmV0LXYzMy4womdoxCAmCyAJoJOohot5WHIvpeVG7eftF+TYXEx4r7BFJpDt0qJsds00mqRub3RlxAjqABVHQ2y/lqNyY3bEIHts4k/rW6zAsWTinCIsV/X2PcOH1DkEglhBHF/hD3wCo3NuZMQg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGkdHlwZaNwYXk=" + gh := byteFromBase64("JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=") + + params := types.SuggestedParams{ + Fee: 4, + FirstRoundValid: 12466, + LastRoundValid: 13466, + GenesisID: "devnet-v33.0", + GenesisHash: gh, + } + txn, err := MakePaymentTxn(fromAddress, toAddress, 1000, byteFromBase64("6gAVR0Nsv5Y="), "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", params) + require.NoError(t, err) + + key, err := mnemonic.ToPrivateKey(mn) + require.NoError(t, err) + + id, bytes, err := crypto.SignTransaction(key, txn) + + stxBytes := byteFromBase64(golden) + require.Equal(t, stxBytes, bytes) + + require.Equal(t, referenceTxID, id) +} + +// should fail on a lack of GenesisHash +func TestMakePaymentTxn2(t *testing.T) { + const fromAddress = "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" + const toAddress = "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI" + params := types.SuggestedParams{ + Fee: 4, + FirstRoundValid: 12466, + LastRoundValid: 13466, + GenesisID: "devnet-v33.0", + } + _, err := MakePaymentTxn(fromAddress, toAddress, 1000, byteFromBase64("6gAVR0Nsv5Y="), "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", params) + require.Error(t, err) +} + +func TestMakePaymentTxnWithLease(t *testing.T) { + const fromAddress = "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" + const toAddress = "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI" + const referenceTxID = "7BG6COBZKF6I6W5XY72ZE4HXV6LLZ6ENSR6DASEGSTXYXR4XJOOQ" + const mn = "advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor" + const golden = "gqNzaWfEQOMmFSIKsZvpW0txwzhmbgQjxv6IyN7BbV5sZ2aNgFbVcrWUnqPpQQxfPhV/wdu9jzEPUU1jAujYtcNCxJ7ONgejdHhujKNhbXTNA+ilY2xvc2XEIEDpNJKIJWTLzpxZpptnVCaJ6aHDoqnqW2Wm6KRCH/xXo2ZlZc0FLKJmds0wsqNnZW6sZGV2bmV0LXYzMy4womdoxCAmCyAJoJOohot5WHIvpeVG7eftF+TYXEx4r7BFJpDt0qJsds00mqJseMQgAQIDBAECAwQBAgMEAQIDBAECAwQBAgMEAQIDBAECAwSkbm90ZcQI6gAVR0Nsv5ajcmN2xCB7bOJP61uswLFk4pwiLFf19j3Dh9Q5BIJYQRxf4Q98AqNzbmTEIOfw+E0GgR358xyNh4sRVfRnHVGhhcIAkIZn9ElYcGihpHR5cGWjcGF5" + gh := byteFromBase64("JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=") + params := types.SuggestedParams{ + Fee: 4, + FirstRoundValid: 12466, + LastRoundValid: 13466, + GenesisID: "devnet-v33.0", + GenesisHash: gh, + } + lease := [32]byte{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4} + txn, err := MakePaymentTxn(fromAddress, toAddress, 1000, byteFromBase64("6gAVR0Nsv5Y="), "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", params) + require.NoError(t, err) + txn.AddLease(lease, 4) + require.NoError(t, err) + + key, err := mnemonic.ToPrivateKey(mn) + require.NoError(t, err) + + id, stxBytes, err := crypto.SignTransaction(key, txn) + + goldenBytes := byteFromBase64(golden) + require.Equal(t, goldenBytes, stxBytes) + require.Equal(t, referenceTxID, id) +} + +func TestKeyRegTxn(t *testing.T) { + // preKeyRegTxn is an unsigned signed keyreg txn with zero Sender + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + a, err := types.DecodeAddress(addr) + require.NoError(t, err) + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + expKeyRegTxn := types.Transaction{ + Type: types.KeyRegistrationTx, + Header: types.Header{ + Sender: a, + Fee: 1000, + FirstValid: 322575, + LastValid: 323575, + GenesisHash: byte32ArrayFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="), + GenesisID: "", + }, + KeyregTxnFields: types.KeyregTxnFields{ + VotePK: byte32ArrayFromBase64("Kv7QI7chi1y6axoy+t7wzAVpePqRq/rkjzWh/RMYyLo="), + SelectionPK: byte32ArrayFromBase64("bPgrv4YogPcdaUAxrt1QysYZTVyRAuUMD4zQmCu9llc="), + VoteFirst: 10000, + VoteLast: 10111, + VoteKeyDilution: 11, + }, + } + const signedGolden = "gqNzaWfEQEA8ANbrvTRxU9c8v6WERcEPw7D/HacRgg4vICa61vEof60Wwtx6KJKDyvBuvViFeacLlngPY6vYCVP0DktTwQ2jdHhui6NmZWXNA+iiZnbOAATsD6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAATv96ZzZWxrZXnEIGz4K7+GKID3HWlAMa7dUMrGGU1ckQLlDA+M0JgrvZZXo3NuZMQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2kdHlwZaZrZXlyZWendm90ZWZzdM0nEKZ2b3Rla2QLp3ZvdGVrZXnEICr+0CO3IYtcumsaMvre8MwFaXj6kav65I81of0TGMi6p3ZvdGVsc3TNJ38=" + // now, sign + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + txid, newStxBytes, err := crypto.SignTransaction(private, expKeyRegTxn) + require.NoError(t, err) + require.Equal(t, "MDRIUVH5AW4Z3GMOB67WP44LYLEVM2MP3ZEPKFHUB5J47A2J6TUQ", txid) + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeKeyRegTxn(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + ghAsArray := byte32ArrayFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: 322575, + LastRoundValid: 323575, + GenesisHash: ghAsArray[:], + } + tx, err := MakeKeyRegTxn(addr, []byte{45, 67}, params, "Kv7QI7chi1y6axoy+t7wzAVpePqRq/rkjzWh/RMYyLo=", "bPgrv4YogPcdaUAxrt1QysYZTVyRAuUMD4zQmCu9llc=", 10000, 10111, 11) + require.NoError(t, err) + + a, err := types.DecodeAddress(addr) + require.NoError(t, err) + expKeyRegTxn := types.Transaction{ + Type: types.KeyRegistrationTx, + Header: types.Header{ + Sender: a, + Fee: 3060, + FirstValid: 322575, + LastValid: 323575, + Note: []byte{45, 67}, + GenesisHash: byte32ArrayFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="), + GenesisID: "", + }, + KeyregTxnFields: types.KeyregTxnFields{ + VotePK: byte32ArrayFromBase64("Kv7QI7chi1y6axoy+t7wzAVpePqRq/rkjzWh/RMYyLo="), + SelectionPK: byte32ArrayFromBase64("bPgrv4YogPcdaUAxrt1QysYZTVyRAuUMD4zQmCu9llc="), + VoteFirst: 10000, + VoteLast: 10111, + VoteKeyDilution: 11, + }, + } + require.Equal(t, expKeyRegTxn, tx) +} + +func TestMakeAssetCreateTxn(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const defaultFrozen = false + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const total = 100 + const reserve = addr + const freeze = addr + const clawback = addr + const unitName = "tst" + const assetName = "testcoin" + const testURL = "website" + const metadataHash = "fACPO4nRgO55j1ndAK3W6Sgc4APkcyFh" + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: 322575, + LastRoundValid: 323575, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetCreateTxn(addr, nil, params, total, 0, defaultFrozen, addr, reserve, freeze, clawback, unitName, assetName, testURL, metadataHash) + require.NoError(t, err) + + a, err := types.DecodeAddress(addr) + require.NoError(t, err) + expectedAssetCreationTxn := types.Transaction{ + Type: types.AssetConfigTx, + Header: types.Header{ + Sender: a, + Fee: 4020, + FirstValid: 322575, + LastValid: 323575, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + expectedAssetCreationTxn.AssetParams = types.AssetParams{ + Total: total, + DefaultFrozen: defaultFrozen, + Manager: a, + Reserve: a, + Freeze: a, + Clawback: a, + UnitName: unitName, + AssetName: assetName, + URL: testURL, + } + copy(expectedAssetCreationTxn.AssetParams.MetadataHash[:], []byte(metadataHash)) + require.Equal(t, expectedAssetCreationTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQEDd1OMRoQI/rzNlU4iiF50XQXmup3k5czI9hEsNqHT7K4KsfmA/0DUVkbzOwtJdRsHS8trm3Arjpy9r7AXlbAujdHhuh6RhcGFyiaJhbcQgZkFDUE80blJnTzU1ajFuZEFLM1c2U2djNEFQa2N5RmiiYW6odGVzdGNvaW6iYXWnd2Vic2l0ZaFjxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aFmxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aFtxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aFyxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aF0ZKJ1bqN0c3SjZmVlzQ+0omZ2zgAE7A+iZ2jEIEhjtRiks8hOyBDyLU8QgcsPcfBZp6wg3sYvf3DlCToiomx2zgAE7/ejc25kxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aR0eXBlpGFjZmc=" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetCreateTxnWithDecimals(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const defaultFrozen = false + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const total = 100 + const decimals = 1 + const reserve = addr + const freeze = addr + const clawback = addr + const unitName = "tst" + const assetName = "testcoin" + const testURL = "website" + const metadataHash = "fACPO4nRgO55j1ndAK3W6Sgc4APkcyFh" + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: 322575, + LastRoundValid: 323575, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetCreateTxn(addr, nil, params, total, decimals, defaultFrozen, addr, reserve, freeze, clawback, unitName, assetName, testURL, metadataHash) + require.NoError(t, err) + + a, err := types.DecodeAddress(addr) + require.NoError(t, err) + expectedAssetCreationTxn := types.Transaction{ + Type: types.AssetConfigTx, + Header: types.Header{ + Sender: a, + Fee: 4060, + FirstValid: 322575, + LastValid: 323575, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + expectedAssetCreationTxn.AssetParams = types.AssetParams{ + Total: total, + Decimals: decimals, + DefaultFrozen: defaultFrozen, + Manager: a, + Reserve: a, + Freeze: a, + Clawback: a, + UnitName: unitName, + AssetName: assetName, + URL: testURL, + } + copy(expectedAssetCreationTxn.AssetParams.MetadataHash[:], []byte(metadataHash)) + require.Equal(t, expectedAssetCreationTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQCj5xLqNozR5ahB+LNBlTG+d0gl0vWBrGdAXj1ibsCkvAwOsXs5KHZK1YdLgkdJecQiWm4oiZ+pm5Yg0m3KFqgqjdHhuh6RhcGFyiqJhbcQgZkFDUE80blJnTzU1ajFuZEFLM1c2U2djNEFQa2N5RmiiYW6odGVzdGNvaW6iYXWnd2Vic2l0ZaFjxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aJkYwGhZsQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2hbcQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2hcsQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2hdGSidW6jdHN0o2ZlZc0P3KJmds4ABOwPomdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4ABO/3o3NuZMQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2kdHlwZaRhY2Zn" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetConfigTxn(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const manager = addr + const reserve = addr + const freeze = addr + const clawback = addr + const assetIndex = 1234 + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: 322575, + LastRoundValid: 323575, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetConfigTxn(addr, nil, params, assetIndex, manager, reserve, freeze, clawback, false) + require.NoError(t, err) + + a, err := types.DecodeAddress(addr) + require.NoError(t, err) + expectedAssetConfigTxn := types.Transaction{ + Type: types.AssetConfigTx, + Header: types.Header{ + Sender: a, + Fee: 3400, + FirstValid: 322575, + LastValid: 323575, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + + expectedAssetConfigTxn.AssetParams = types.AssetParams{ + Manager: a, + Reserve: a, + Freeze: a, + Clawback: a, + } + expectedAssetConfigTxn.ConfigAsset = types.AssetIndex(assetIndex) + require.Equal(t, expectedAssetConfigTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQBBkfw5n6UevuIMDo2lHyU4dS80JCCQ/vTRUcTx5m0ivX68zTKyuVRrHaTbxbRRc3YpJ4zeVEnC9Fiw3Wf4REwejdHhuiKRhcGFyhKFjxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aFmxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aFtxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aFyxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aRjYWlkzQTSo2ZlZc0NSKJmds4ABOwPomdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4ABO/3o3NuZMQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2kdHlwZaRhY2Zn" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetConfigTxnStrictChecking(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const manager = addr + const reserve = addr + const freeze = "" + const clawback = addr + const assetIndex = 1234 + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: 322575, + LastRoundValid: 323575, + GenesisHash: ghAsArray[:], + } + _, err := MakeAssetConfigTxn(addr, nil, params, assetIndex, manager, reserve, freeze, clawback, true) + require.Error(t, err) +} + +func TestMakeAssetDestroyTxn(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const creator = addr + const assetIndex = 1 + const firstValidRound = 322575 + const lastValidRound = 323575 + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: firstValidRound, + LastRoundValid: lastValidRound, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetDestroyTxn(creator, nil, params, assetIndex) + require.NoError(t, err) + + a, err := types.DecodeAddress(creator) + require.NoError(t, err) + + expectedAssetDestroyTxn := types.Transaction{ + Type: types.AssetConfigTx, + Header: types.Header{ + Sender: a, + Fee: 1880, + FirstValid: firstValidRound, + LastValid: lastValidRound, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + expectedAssetDestroyTxn.AssetParams = types.AssetParams{} + expectedAssetDestroyTxn.ConfigAsset = types.AssetIndex(assetIndex) + require.Equal(t, expectedAssetDestroyTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQBSP7HtzD/Lvn4aVvaNpeR4T93dQgo4LvywEwcZgDEoc/WVl3aKsZGcZkcRFoiWk8AidhfOZzZYutckkccB8RgGjdHhuh6RjYWlkAaNmZWXNB1iiZnbOAATsD6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAATv96NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pHR5cGWkYWNmZw==" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetFreezeTxn(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const assetIndex = 1 + const firstValidRound = 322575 + const lastValidRound = 323576 + const freezeSetting = true + const target = addr + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: firstValidRound, + LastRoundValid: lastValidRound, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetFreezeTxn(addr, nil, params, assetIndex, target, freezeSetting) + require.NoError(t, err) + + a, err := types.DecodeAddress(addr) + require.NoError(t, err) + + expectedAssetFreezeTxn := types.Transaction{ + Type: types.AssetFreezeTx, + Header: types.Header{ + Sender: a, + Fee: 2330, + FirstValid: firstValidRound, + LastValid: lastValidRound, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + expectedAssetFreezeTxn.FreezeAsset = types.AssetIndex(assetIndex) + expectedAssetFreezeTxn.AssetFrozen = freezeSetting + expectedAssetFreezeTxn.FreezeAccount = a + require.Equal(t, expectedAssetFreezeTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQAhru5V2Xvr19s4pGnI0aslqwY4lA2skzpYtDTAN9DKSH5+qsfQQhm4oq+9VHVj7e1rQC49S28vQZmzDTVnYDQGjdHhuiaRhZnJ6w6RmYWRkxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aRmYWlkAaNmZWXNCRqiZnbOAATsD6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAATv+KNzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pHR5cGWkYWZyeg==" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetTransferTxn(t *testing.T) { + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const sender, recipient, closeAssetsTo = addr, addr, addr + const assetIndex = 1 + const firstValidRound = 322575 + const lastValidRound = 323576 + const amountToSend = 1 + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: firstValidRound, + LastRoundValid: lastValidRound, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetTransferTxn(sender, recipient, amountToSend, nil, params, closeAssetsTo, assetIndex) + require.NoError(t, err) + + sendAddr, err := types.DecodeAddress(sender) + require.NoError(t, err) + + expectedAssetTransferTxn := types.Transaction{ + Type: types.AssetTransferTx, + Header: types.Header{ + Sender: sendAddr, + Fee: 2750, + FirstValid: firstValidRound, + LastValid: lastValidRound, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + + expectedAssetID := types.AssetIndex(assetIndex) + expectedAssetTransferTxn.XferAsset = expectedAssetID + + receiveAddr, err := types.DecodeAddress(recipient) + require.NoError(t, err) + expectedAssetTransferTxn.AssetReceiver = receiveAddr + + closeAddr, err := types.DecodeAddress(closeAssetsTo) + require.NoError(t, err) + expectedAssetTransferTxn.AssetCloseTo = closeAddr + + expectedAssetTransferTxn.AssetAmount = amountToSend + + require.Equal(t, expectedAssetTransferTxn, tx) + + // now compare tx against a golden + const signedGolden = "gqNzaWfEQNkEs3WdfFq6IQKJdF1n0/hbV9waLsvojy9pM1T4fvwfMNdjGQDy+LeesuQUfQVTneJD4VfMP7zKx4OUlItbrwSjdHhuiqRhYW10AaZhY2xvc2XEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pGFyY3bEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9o2ZlZc0KvqJmds4ABOwPomdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4ABO/4o3NuZMQgCfvSdiwI+Gxa5r9t16epAd5mdddQ4H6MXHaYZH224f2kdHlwZaVheGZlcqR4YWlkAQ==" + _, newStxBytes, err := crypto.SignTransaction(private, tx) + require.NoError(t, err) + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetAcceptanceTxn(t *testing.T) { + const sender = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const assetIndex = 1 + const firstValidRound = 322575 + const lastValidRound = 323575 + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: firstValidRound, + LastRoundValid: lastValidRound, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetAcceptanceTxn(sender, nil, params, assetIndex) + require.NoError(t, err) + + sendAddr, err := types.DecodeAddress(sender) + require.NoError(t, err) + + expectedAssetAcceptanceTxn := types.Transaction{ + Type: types.AssetTransferTx, + Header: types.Header{ + Sender: sendAddr, + Fee: 2280, + FirstValid: firstValidRound, + LastValid: lastValidRound, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + + expectedAssetID := types.AssetIndex(assetIndex) + expectedAssetAcceptanceTxn.XferAsset = expectedAssetID + expectedAssetAcceptanceTxn.AssetReceiver = sendAddr + expectedAssetAcceptanceTxn.AssetAmount = 0 + + require.Equal(t, expectedAssetAcceptanceTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQJ7q2rOT8Sb/wB0F87ld+1zMprxVlYqbUbe+oz0WM63FctIi+K9eYFSqT26XBZ4Rr3+VTJpBE+JLKs8nctl9hgijdHhuiKRhcmN2xCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aNmZWXNCOiiZnbOAATsD6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAATv96NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pHR5cGWlYXhmZXKkeGFpZAE=" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestMakeAssetRevocationTransaction(t *testing.T) { + const addr = "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4" + const genesisHash = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ghAsArray := byte32ArrayFromBase64(genesisHash) + const revoker, recipient, revoked = addr, addr, addr + const assetIndex = 1 + const firstValidRound = 322575 + const lastValidRound = 323575 + const amountToSend = 1 + + params := types.SuggestedParams{ + Fee: 10, + FirstRoundValid: firstValidRound, + LastRoundValid: lastValidRound, + GenesisHash: ghAsArray[:], + } + tx, err := MakeAssetRevocationTxn(revoker, revoked, amountToSend, recipient, nil, params, assetIndex) + require.NoError(t, err) + + sendAddr, err := types.DecodeAddress(revoker) + require.NoError(t, err) + + expectedAssetRevocationTxn := types.Transaction{ + Type: types.AssetTransferTx, + Header: types.Header{ + Sender: sendAddr, + Fee: 2730, + FirstValid: firstValidRound, + LastValid: lastValidRound, + GenesisHash: byte32ArrayFromBase64(genesisHash), + GenesisID: "", + }, + } + + expectedAssetID := types.AssetIndex(assetIndex) + expectedAssetRevocationTxn.XferAsset = expectedAssetID + + receiveAddr, err := types.DecodeAddress(recipient) + require.NoError(t, err) + expectedAssetRevocationTxn.AssetReceiver = receiveAddr + + expectedAssetRevocationTxn.AssetAmount = amountToSend + + targetAddr, err := types.DecodeAddress(revoked) + require.NoError(t, err) + expectedAssetRevocationTxn.AssetSender = targetAddr + + require.Equal(t, expectedAssetRevocationTxn, tx) + + const addrSK = "awful drop leaf tennis indoor begin mandate discover uncle seven only coil atom any hospital uncover make any climb actor armed measure need above hundred" + private, err := mnemonic.ToPrivateKey(addrSK) + require.NoError(t, err) + _, newStxBytes, err := crypto.SignTransaction(private, tx) + signedGolden := "gqNzaWfEQHsgfEAmEHUxLLLR9s+Y/yq5WeoGo/jAArCbany+7ZYwExMySzAhmV7M7S8+LBtJalB4EhzEUMKmt3kNKk6+vAWjdHhuiqRhYW10AaRhcmN2xCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aRhc25kxCAJ+9J2LAj4bFrmv23Xp6kB3mZ111Dgfoxcdphkfbbh/aNmZWXNCqqiZnbOAATsD6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAATv96NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pHR5cGWlYXhmZXKkeGFpZAE=" + require.EqualValues(t, newStxBytes, byteFromBase64(signedGolden)) +} + +func TestComputeGroupID(t *testing.T) { + // compare regular transactions created in SDK with 'goal clerk send' result + // compare transaction group created in SDK with 'goal clerk group' result + const address = "UPYAFLHSIPMJOHVXU2MPLQ46GXJKSDCEMZ6RLCQ7GWB5PRDKJUWKKXECXI" + const fromAddress, toAddress = address, address + const fee = 1000 + const amount = 2000 + const genesisID = "devnet-v1.0" + genesisHash := byteFromBase64("sC3P7e2SdbqKJK0tbiCdK9tdSpbe6XeCGKdoNzmlj0E=") + + const firstRound1 = 710399 + params1 := types.SuggestedParams{ + Fee: fee, + FirstRoundValid: firstRound1, + LastRoundValid: firstRound1 + 1000, + GenesisHash: genesisHash, + GenesisID: genesisID, + FlatFee: true, + } + note1 := byteFromBase64("wRKw5cJ0CMo=") + tx1, err := MakePaymentTxn(fromAddress, toAddress, amount, note1, "", params1) + require.NoError(t, err) + + const firstRound2 = 710515 + params2 := types.SuggestedParams{ + Fee: fee, + FirstRoundValid: firstRound2, + LastRoundValid: firstRound2 + 1000, + GenesisHash: genesisHash, + GenesisID: genesisID, + FlatFee: true, + } + note2 := byteFromBase64("dBlHI6BdrIg=") + tx2, err := MakePaymentTxn(fromAddress, toAddress, amount, note2, "", params2) + require.NoError(t, err) + + const goldenTx1 = "gaN0eG6Ko2FtdM0H0KNmZWXNA+iiZnbOAArW/6NnZW6rZGV2bmV0LXYxLjCiZ2jEILAtz+3tknW6iiStLW4gnSvbXUqW3ul3ghinaDc5pY9Bomx2zgAK2uekbm90ZcQIwRKw5cJ0CMqjcmN2xCCj8AKs8kPYlx63ppj1w5410qkMRGZ9FYofNYPXxGpNLKNzbmTEIKPwAqzyQ9iXHremmPXDnjXSqQxEZn0Vih81g9fEak0spHR5cGWjcGF5" + const goldenTx2 = "gaN0eG6Ko2FtdM0H0KNmZWXNA+iiZnbOAArXc6NnZW6rZGV2bmV0LXYxLjCiZ2jEILAtz+3tknW6iiStLW4gnSvbXUqW3ul3ghinaDc5pY9Bomx2zgAK21ukbm90ZcQIdBlHI6BdrIijcmN2xCCj8AKs8kPYlx63ppj1w5410qkMRGZ9FYofNYPXxGpNLKNzbmTEIKPwAqzyQ9iXHremmPXDnjXSqQxEZn0Vih81g9fEak0spHR5cGWjcGF5" + + // goal clerk send dumps unsigned transaction as signed with empty signature in order to save tx type + stx1 := types.SignedTxn{Sig: types.Signature{}, Msig: types.MultisigSig{}, Txn: tx1} + stx2 := types.SignedTxn{Sig: types.Signature{}, Msig: types.MultisigSig{}, Txn: tx2} + require.Equal(t, byteFromBase64(goldenTx1), msgpack.Encode(stx1)) + require.Equal(t, byteFromBase64(goldenTx2), msgpack.Encode(stx2)) + + gid, err := crypto.ComputeGroupID([]types.Transaction{tx1, tx2}) + + // goal clerk group sets Group to every transaction and concatenate them in output file + // simulating that behavior here + stx1.Txn.Group = gid + stx2.Txn.Group = gid + + var txg []byte + txg = append(txg, msgpack.Encode(stx1)...) + txg = append(txg, msgpack.Encode(stx2)...) + + const goldenTxg = "gaN0eG6Lo2FtdM0H0KNmZWXNA+iiZnbOAArW/6NnZW6rZGV2bmV0LXYxLjCiZ2jEILAtz+3tknW6iiStLW4gnSvbXUqW3ul3ghinaDc5pY9Bo2dycMQgLiQ9OBup9H/bZLSfQUH2S6iHUM6FQ3PLuv9FNKyt09SibHbOAAra56Rub3RlxAjBErDlwnQIyqNyY3bEIKPwAqzyQ9iXHremmPXDnjXSqQxEZn0Vih81g9fEak0so3NuZMQgo/ACrPJD2Jcet6aY9cOeNdKpDERmfRWKHzWD18RqTSykdHlwZaNwYXmBo3R4boujYW10zQfQo2ZlZc0D6KJmds4ACtdzo2dlbqtkZXZuZXQtdjEuMKJnaMQgsC3P7e2SdbqKJK0tbiCdK9tdSpbe6XeCGKdoNzmlj0GjZ3JwxCAuJD04G6n0f9tktJ9BQfZLqIdQzoVDc8u6/0U0rK3T1KJsds4ACttbpG5vdGXECHQZRyOgXayIo3JjdsQgo/ACrPJD2Jcet6aY9cOeNdKpDERmfRWKHzWD18RqTSyjc25kxCCj8AKs8kPYlx63ppj1w5410qkMRGZ9FYofNYPXxGpNLKR0eXBlo3BheQ==" + + require.Equal(t, byteFromBase64(goldenTxg), txg) + + // check transaction.AssignGroupID, do not validate correctness of Group field calculation + result, err := transaction.AssignGroupID([]types.Transaction{tx1, tx2}, "BH55E5RMBD4GYWXGX5W5PJ5JAHPGM5OXKDQH5DC4O2MGI7NW4H6VOE4CP4") + require.NoError(t, err) + require.Equal(t, 0, len(result)) + + result, err = transaction.AssignGroupID([]types.Transaction{tx1, tx2}, address) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + + result, err = transaction.AssignGroupID([]types.Transaction{tx1, tx2}, "") + require.NoError(t, err) + require.Equal(t, 2, len(result)) +} + +func TestLogicSig(t *testing.T) { + // validate LogicSig signed transaction against goal + const fromAddress = "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU" + const toAddress = "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI" + const referenceTxID = "5FJDJD5LMZC3EHUYYJNH5I23U4X6H2KXABNDGPIL557ZMJ33GZHQ" + const mn = "advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor" + const fee = 1000 + const amount = 2000 + const firstRound = 2063137 + const genesisID = "devnet-v1.0" + genesisHash := byteFromBase64("sC3P7e2SdbqKJK0tbiCdK9tdSpbe6XeCGKdoNzmlj0E=") + note := byteFromBase64("8xMCTuLQ810=") + + params := types.SuggestedParams{ + Fee: fee, + FirstRoundValid: firstRound, + LastRoundValid: firstRound + 1000, + GenesisHash: genesisHash, + GenesisID: genesisID, + FlatFee: true, + } + tx, err := MakePaymentTxn(fromAddress, toAddress, amount, note, "", params) + require.NoError(t, err) + + // goal clerk send -o tx3 -a 2000 --fee 1000 -d ~/.algorand -w test -L sig.lsig --argb64 MTIz --argb64 NDU2 \ + // -f 47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU \ + // -t PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI + const golden = "gqRsc2lng6NhcmeSxAMxMjPEAzQ1NqFsxAUBIAEBIqNzaWfEQE6HXaI5K0lcq50o/y3bWOYsyw9TLi/oorZB4xaNdn1Z14351u2f6JTON478fl+JhIP4HNRRAIh/I8EWXBPpJQ2jdHhuiqNhbXTNB9CjZmVlzQPoomZ2zgAfeyGjZ2Vuq2Rldm5ldC12MS4womdoxCCwLc/t7ZJ1uookrS1uIJ0r211Klt7pd4IYp2g3OaWPQaJsds4AH38JpG5vdGXECPMTAk7i0PNdo3JjdsQge2ziT+tbrMCxZOKcIixX9fY9w4fUOQSCWEEcX+EPfAKjc25kxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaR0eXBlo3BheQ==" + + program := []byte{1, 32, 1, 1, 34} + args := make([][]byte, 2) + args[0] = []byte("123") + args[1] = []byte("456") + key, err := mnemonic.ToPrivateKey(mn) + var pk crypto.MultisigAccount + require.NoError(t, err) + lsig, err := crypto.MakeLogicSig(program, args, key, pk) + require.NoError(t, err) + + _, stxBytes, err := crypto.SignLogicsigTransaction(lsig, tx) + require.NoError(t, err) + + require.Equal(t, byteFromBase64(golden), stxBytes) + + sender, err := types.DecodeAddress(fromAddress) + require.NoError(t, err) + + verified := crypto.VerifyLogicSig(lsig, sender) + require.True(t, verified) + +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..45bd57c8 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/algorand/go-algorand-sdk + +go 1.13 + +require ( + github.com/algorand/go-codec v1.1.7 + github.com/algorand/go-codec/codec v1.1.7 + github.com/cucumber/godog v0.8.1 + github.com/davecgh/go-spew v1.1.1 + github.com/google/go-querystring v1.0.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/stretchr/testify v1.3.0 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..cb84b0de --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/algorand/go-codec v1.1.6-algorand h1:zdBZ0nPfsiho3JHnkaGyuUOLZQonbtFe9eOEcor1HlE= +github.com/algorand/go-codec v1.1.6-algorand/go.mod h1:A3YI4V24jUUnU1eNekNmx2fLi60FvlNssqOiUsyfNM8= +github.com/algorand/go-codec v1.1.7 h1:6nvCh2nfgnfkaoVHKQyk2wxyl2GQBAlI7IkbqbB/e4s= +github.com/algorand/go-codec v1.1.7/go.mod h1:pVLQYhIVCsx9D3iy4W4Qqi0SKhx6IVhMwOvj/agFL4g= +github.com/algorand/go-codec/codec v1.1.7 h1:EFOyWf5duxbh2ru+AW1YDgmZ+MRVgqklELSqTArgp3M= +github.com/algorand/go-codec/codec v1.1.7/go.mod h1:xahKG+YDWbJCG+5M1Qkh1X+Qec4IlDVfWMeRTWYABz4= +github.com/cucumber/godog v0.8.1 h1:lVb+X41I4YDreE+ibZ50bdXmySxgRviYFgKY6Aw4XE8= +github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/logic/logic.go b/logic/logic.go index 6a4972d6..3c278a65 100644 --- a/logic/logic.go +++ b/logic/logic.go @@ -34,24 +34,33 @@ var opcodes []operation // CheckProgram performs basic program validation: instruction count and program cost func CheckProgram(program []byte, args [][]byte) error { + _, _, err := ReadProgram(program, args) + return err +} + +// ReadProgram is used to validate a program as well as extract found variables +func ReadProgram(program []byte, args [][]byte) (ints []uint64, byteArrays [][]byte, err error) { const intcblockOpcode = 32 const bytecblockOpcode = 38 if program == nil || len(program) == 0 { - return fmt.Errorf("empty program") + err = fmt.Errorf("empty program") + return } if spec == nil { spec = new(langSpec) - if err := json.Unmarshal(langSpecJson, spec); err != nil { - return err + if err = json.Unmarshal(langSpecJson, spec); err != nil { + return } } version, vlen := binary.Uvarint(program) if vlen <= 0 { - return fmt.Errorf("version parsing error") + err = fmt.Errorf("version parsing error") + return } if int(version) > spec.EvalMaxVersion { - return fmt.Errorf("unsupported version") + err = fmt.Errorf("unsupported version") + return } cost := 0 @@ -60,7 +69,8 @@ func CheckProgram(program []byte, args [][]byte) error { length += len(arg) } if length > types.LogicSigMaxSize { - return fmt.Errorf("program too long") + err = fmt.Errorf("program too long") + return } if opcodes == nil { @@ -73,39 +83,44 @@ func CheckProgram(program []byte, args [][]byte) error { for pc := vlen; pc < len(program); { op := opcodes[program[pc]] if op.Name == "" { - return fmt.Errorf("invalid instruction") + err = fmt.Errorf("invalid instruction") + return } cost = cost + op.Cost size := op.Size - var err error if size == 0 { switch op.Opcode { case intcblockOpcode: - size, err = checkIntConstBlock(program, pc) + var foundInts []uint64 + size, foundInts, err = readIntConstBlock(program, pc) + ints = append(ints, foundInts...) if err != nil { - return err + return } case bytecblockOpcode: - size, err = checkByteConstBlock(program, pc) + var foundByteArrays [][]byte + size, foundByteArrays, err = readByteConstBlock(program, pc) + byteArrays = append(byteArrays, foundByteArrays...) if err != nil { - return err + return } default: - return fmt.Errorf("invalid instruction") + err = fmt.Errorf("invalid instruction") + return } } pc = pc + size } if cost > types.LogicSigMaxCost { - return fmt.Errorf("program too costly to run") + err = fmt.Errorf("program too costly to run") } - return nil + return } -func checkIntConstBlock(program []byte, pc int) (size int, err error) { +func readIntConstBlock(program []byte, pc int) (size int, ints []uint64, err error) { size = 1 numInts, bytesUsed := binary.Uvarint(program[pc+size:]) if bytesUsed <= 0 { @@ -119,17 +134,18 @@ func checkIntConstBlock(program []byte, pc int) (size int, err error) { err = fmt.Errorf("intcblock ran past end of program") return } - _, bytesUsed = binary.Uvarint(program[pc+size:]) + num, bytesUsed := binary.Uvarint(program[pc+size:]) if bytesUsed <= 0 { err = fmt.Errorf("could not decode int const[%d] at pc=%d", i, pc+size) return } + ints = append(ints, num) size += bytesUsed } return } -func checkByteConstBlock(program []byte, pc int) (size int, err error) { +func readByteConstBlock(program []byte, pc int) (size int, byteArrays [][]byte, err error) { size = 1 numInts, bytesUsed := binary.Uvarint(program[pc+size:]) if bytesUsed <= 0 { @@ -143,7 +159,8 @@ func checkByteConstBlock(program []byte, pc int) (size int, err error) { err = fmt.Errorf("bytecblock ran past end of program") return } - itemLen, bytesUsed := binary.Uvarint(program[pc+size:]) + scanTarget := program[pc+size:] + itemLen, bytesUsed := binary.Uvarint(scanTarget) if bytesUsed <= 0 { err = fmt.Errorf("could not decode []byte const[%d] at pc=%d", i, pc+size) return @@ -153,6 +170,8 @@ func checkByteConstBlock(program []byte, pc int) (size int, err error) { err = fmt.Errorf("bytecblock ran past end of program") return } + byteArray := program[pc+size : pc+size+int(itemLen)] + byteArrays = append(byteArrays, byteArray) size += int(itemLen) } return diff --git a/templates/dynamicFee.go b/templates/dynamicFee.go new file mode 100644 index 00000000..1f5811b2 --- /dev/null +++ b/templates/dynamicFee.go @@ -0,0 +1,183 @@ +package templates + +import ( + "encoding/base64" + "fmt" + "github.com/algorand/go-algorand-sdk/crypto" + "github.com/algorand/go-algorand-sdk/future" + "github.com/algorand/go-algorand-sdk/logic" + "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/types" + "golang.org/x/crypto/ed25519" +) + +// DynamicFee template representation +type DynamicFee struct { + ContractTemplate +} + +// MakeDynamicFee contract allows you to create a transaction without +// specifying the fee. The fee will be determined at the moment of +// transfer. +// +// Parameters: +// - receiver: address which is authorized to receive withdrawals +// - closeRemainder: address which will receive the balance of funds +// - amount: the maximum number of funds allowed for a single withdrawal +// - withdrawWindow: the duration of a withdrawal period +// - period: the time between a pair of withdrawal periods +// - expiryRound: the round at which the account expires +// - maxFee: maximum fee used by the withdrawal transaction +func MakeDynamicFee(receiver, closeRemainder string, amount, firstValid, lastValid uint64) (DynamicFee, error) { + leaseBytes := make([]byte, 32) + crypto.RandomBytes(leaseBytes) + leaseString := base64.StdEncoding.EncodeToString(leaseBytes) + return makeDynamicFeeWithLease(receiver, closeRemainder, leaseString, amount, firstValid, lastValid) +} + +// makeDynamicFeeWithLease is as MakeDynamicFee, but the caller can specify the lease (using b64 string) +func makeDynamicFeeWithLease(receiver, closeRemainder, lease string, amount, firstValid, lastValid uint64) (DynamicFee, error) { + const referenceProgram = "ASAFAgEHBgUmAyD+vKC7FEpaTqe0OKRoGsgObKEFvLYH/FZTJclWlfaiEyDmmpYeby1feshmB5JlUr6YI17TM2PKiJGLuck4qRW2+SB/g7Flf/H8U7ktwYFIodZd/C1LH6PWdyhK3dIAEm2QaTIEIhIzABAjEhAzAAcxABIQMwAIMQESEDEWIxIQMRAjEhAxBygSEDEJKRIQMQgkEhAxAiUSEDEEIQQSEDEGKhIQ" + referenceAsBytes, err := base64.StdEncoding.DecodeString(referenceProgram) + if err != nil { + return DynamicFee{}, err + } + receiverAddr, err := types.DecodeAddress(receiver) + if err != nil { + return DynamicFee{}, err + } + var closeRemainderAddr types.Address + if closeRemainder != "" { + closeRemainderAddr, err = types.DecodeAddress(closeRemainder) + if err != nil { + return DynamicFee{}, err + } + } + + var referenceOffsets = []uint64{ /*amount*/ 5 /*firstValid*/, 6 /*lastValid*/, 7 /*receiver*/, 11 /*closeRemainder*/, 44 /*lease*/, 76} + injectionVector := []interface{}{amount, firstValid, lastValid, receiverAddr, closeRemainderAddr, lease} + injectedBytes, err := inject(referenceAsBytes, referenceOffsets, injectionVector) + if err != nil { + return DynamicFee{}, err + } + + address := crypto.AddressFromProgram(injectedBytes) + dynamicFee := DynamicFee{ + ContractTemplate: ContractTemplate{ + address: address.String(), + program: injectedBytes, + }, + } + return dynamicFee, err +} + +// GetDynamicFeeTransactions creates and signs the secondary dynamic fee transaction, updates +// transaction fields, and signs as the fee payer; it returns both +// transactions as bytes suitable for sendRaw. +// Parameters: +// txn - main transaction from payer +// lsig - the signed logic received from the payer +// privateKey - the private key for the account that pays the fee +// fee - fee per byte for both transactions +// firstValid - first protocol round on which both transactions will be valid +// lastValid - last protocol round on which both transactions will be valid +func GetDynamicFeeTransactions(txn types.Transaction, lsig types.LogicSig, privateKey ed25519.PrivateKey, fee uint64) ([]byte, error) { + txn.Fee = types.MicroAlgos(fee) + eSize, err := transaction.EstimateSize(txn) + if err != nil { + return nil, err + } + txn.Fee = types.MicroAlgos(eSize * fee) + + if txn.Fee < transaction.MinTxnFee { + txn.Fee = transaction.MinTxnFee + } + + address := types.Address{} + copy(address[:], privateKey[ed25519.PublicKeySize:]) + genesisHash := make([]byte, 32) + copy(genesisHash[:], txn.GenesisHash[:]) + + params := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisID: txn.GenesisID, + GenesisHash: genesisHash, + FirstRoundValid: txn.FirstValid, + LastRoundValid: txn.LastValid, + FlatFee: false, + } + + feePayTxn, err := future.MakePaymentTxn(address.String(), txn.Sender.String(), uint64(txn.Fee), nil, "", params) + if err != nil { + return nil, err + } + feePayTxn.AddLease(txn.Lease, fee) + + txnGroup := []types.Transaction{feePayTxn, txn} + + updatedTxns, err := transaction.AssignGroupID(txnGroup, "") + + _, stx1Bytes, err := crypto.SignTransaction(privateKey, updatedTxns[0]) + if err != nil { + return nil, err + } + _, stx2Bytes, err := crypto.SignLogicsigTransaction(lsig, updatedTxns[1]) + if err != nil { + return nil, err + } + return append(stx1Bytes, stx2Bytes...), nil +} + +// SignDynamicFee takes in the contract bytes and returns the main transaction and signed logic needed to complete the +// transfer. These should be sent to the fee payer, who can use +// GetDynamicFeeTransactions() to update fields and create the auxiliary +// transaction. +// Parameters: +// contract - the bytearray representing the contract in question +// genesisHash - the bytearray representing the network for the txns +func SignDynamicFee(contract []byte, privateKey ed25519.PrivateKey, genesisHash []byte) (txn types.Transaction, lsig types.LogicSig, err error) { + ints, byteArrays, err := logic.ReadProgram(contract, nil) + if err != nil { + return + } + + // Convert the byteArrays[0] to receiver + var receiver types.Address //byteArrays[0] + n := copy(receiver[:], byteArrays[0]) + if n != ed25519.PublicKeySize { + err = fmt.Errorf("address generated from receiver bytes is the wrong size") + return + } + // Convert the byteArrays[1] to closeRemainderTo + var closeRemainderTo types.Address + n = copy(closeRemainderTo[:], byteArrays[1]) + if n != ed25519.PublicKeySize { + err = fmt.Errorf("address generated from closeRemainderTo bytes is the wrong size") + return + } + contractLease := byteArrays[2] + amount, firstValid, lastValid := ints[2], ints[3], ints[4] + address := types.Address{} + copy(address[:], privateKey[ed25519.PublicKeySize:]) + + fee := uint64(0) + params := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisID: "", + GenesisHash: genesisHash, + FirstRoundValid: types.Round(firstValid), + LastRoundValid: types.Round(lastValid), + FlatFee: false, + } + + txn, err = future.MakePaymentTxn(address.String(), receiver.String(), amount, nil, closeRemainderTo.String(), params) + if err != nil { + return + } + lease := [32]byte{} + copy(lease[:], contractLease) // convert from []byte to [32]byte + txn.AddLease(lease, fee) + lsig, err = crypto.MakeLogicSig(contract, nil, privateKey, crypto.MultisigAccount{}) + + return +} diff --git a/templates/hashTimeLockedContract.go b/templates/hashTimeLockedContract.go index bd0394a0..c783230f 100644 --- a/templates/hashTimeLockedContract.go +++ b/templates/hashTimeLockedContract.go @@ -71,3 +71,21 @@ func MakeHTLC(owner, receiver, hashFunction, hashImage string, expiryRound, maxF } return htlc, err } + +// SignTransactionWithHTLCUnlock accepts a transaction, such as a payment, and builds the HTLC-unlocking signature around that transaction +func SignTransactionWithHTLCUnlock(program []byte, txn types.Transaction, preImageAsBase64 string) (txid string, stx []byte, err error) { + preImageAsArgument, err := base64.StdEncoding.DecodeString(preImageAsBase64) + if err != nil { + return + } + args := make([][]byte, 1) + args[0] = preImageAsArgument + var blankMultisig crypto.MultisigAccount + lsig, err := crypto.MakeLogicSig(program, args, nil, blankMultisig) + if err != nil { + return + } + txn.Receiver = types.Address{} //txn must have no receiver but MakePayment et al disallow this. + txid, stx, err = crypto.SignLogicsigTransaction(lsig, txn) + return +} diff --git a/templates/limitOrder.go b/templates/limitOrder.go index 42787eed..df77979f 100644 --- a/templates/limitOrder.go +++ b/templates/limitOrder.go @@ -3,7 +3,7 @@ package templates import ( "encoding/base64" "github.com/algorand/go-algorand-sdk/crypto" - "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/future" "github.com/algorand/go-algorand-sdk/types" ) @@ -18,27 +18,24 @@ type LimitOrder struct { // assetAmount: amount of assets to be sent // contract: byteform of the contract from the payer // secretKey: secret key for signing transactions -// fee: fee per byte used for the transactions -// algoAmount: number of algos to transfer -// firstRound: first round on which these txns will be valid -// lastRound: last round on which these txns will be valid -// genesisHash: genesisHash indicating the network for the txns +// microAlgoAmount: number of microAlgos to transfer +// params: txn params for the transactions // the first payment sends money (Algos) from contract to the recipient (we'll call him Buyer), closing the rest of the account to Owner // the second payment sends money (the asset) from Buyer to the Owner // these transactions will be rejected if they do not meet the restrictions set by the contract -func (lo LimitOrder) GetSwapAssetsTransaction(assetAmount uint64, contract, secretKey []byte, fee, algoAmount, firstRound, lastRound uint64, genesisHash []byte) ([]byte, error) { +func (lo LimitOrder) GetSwapAssetsTransaction(assetAmount, microAlgoAmount uint64, contract, secretKey []byte, params types.SuggestedParams) ([]byte, error) { var buyerAddress types.Address copy(buyerAddress[:], secretKey[32:]) contractAddress := crypto.AddressFromProgram(contract) - algosForAssets, err := transaction.MakePaymentTxn(contractAddress.String(), buyerAddress.String(), fee, algoAmount, firstRound, lastRound, nil, lo.owner, "", genesisHash) + algosForAssets, err := future.MakePaymentTxn(contractAddress.String(), buyerAddress.String(), microAlgoAmount, nil, "", params) if err != nil { return nil, err } - - assetsForAlgos, err := transaction.MakeAssetTransferTxn(buyerAddress.String(), lo.owner, "", assetAmount, fee, firstRound, lastRound, nil, lo.owner, "", lo.assetID) + assetsForAlgos, err := future.MakeAssetTransferTxn(buyerAddress.String(), lo.owner, assetAmount, nil, params, lo.owner, lo.assetID) if err != nil { return nil, err } + gid, err := crypto.ComputeGroupID([]types.Transaction{algosForAssets, assetsForAlgos}) if err != nil { return nil, err @@ -60,8 +57,8 @@ func (lo LimitOrder) GetSwapAssetsTransaction(assetAmount uint64, contract, secr } var signedGroup []byte - signedGroup = append(signedGroup, assetsForAlgosSigned...) signedGroup = append(signedGroup, algosForAssetsSigned...) + signedGroup = append(signedGroup, assetsForAlgosSigned...) return signedGroup, nil } diff --git a/templates/periodicPayment.go b/templates/periodicPayment.go new file mode 100644 index 00000000..9fb697eb --- /dev/null +++ b/templates/periodicPayment.go @@ -0,0 +1,115 @@ +package templates + +import ( + "encoding/base64" + "fmt" + "github.com/algorand/go-algorand-sdk/crypto" + "github.com/algorand/go-algorand-sdk/future" + "github.com/algorand/go-algorand-sdk/logic" + "github.com/algorand/go-algorand-sdk/types" + "golang.org/x/crypto/ed25519" +) + +// PeriodicPayment template representation +type PeriodicPayment struct { + ContractTemplate +} + +// GetPeriodicPaymentWithdrawalTransaction returns a signed transaction extracting funds from the contract +// contract: the bytearray defining the contract, received from the payer +// firstValid: the first round on which the txn will be valid +// fee: the fee in microalgos per byte of the payment txn +// genesisHash: the hash representing the network for the txn +func GetPeriodicPaymentWithdrawalTransaction(contract []byte, firstValid, fee uint64, genesisHash []byte) ([]byte, error) { + address := crypto.AddressFromProgram(contract) + ints, byteArrays, err := logic.ReadProgram(contract, nil) + if err != nil { + return nil, err + } + contractLease := byteArrays[0] + // Convert the byteArrays[1] to receiver + var receiver types.Address + n := copy(receiver[:], byteArrays[1]) + if n != ed25519.PublicKeySize { + return nil, fmt.Errorf("address generated from receiver bytes is the wrong size") + } + period, withdrawWindow, amount := ints[2], ints[4], ints[5] + if firstValid%period != 0 { + return nil, fmt.Errorf("firstValid round %d was not a multiple of the contract period %d", firstValid, period) + } + lastValid := firstValid + withdrawWindow + params := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisHash: genesisHash, + FirstRoundValid: types.Round(firstValid), + LastRoundValid: types.Round(lastValid), + FlatFee: false, + } + txn, err := future.MakePaymentTxn(address.String(), receiver.String(), amount, nil, "", params) + if err != nil { + return nil, err + } + lease := [32]byte{} + copy(lease[:], contractLease) // convert from []byte to [32]byte + txn.AddLease(lease, fee) + + logicSig, err := crypto.MakeLogicSig(contract, nil, nil, crypto.MultisigAccount{}) + if err != nil { + return nil, err + } + _, signedTxn, err := crypto.SignLogicsigTransaction(logicSig, txn) + return signedTxn, err +} + +// MakePeriodicPayment allows some account to execute periodic withdrawal of funds. +// This is a contract account. +// +// This allows receiver to withdraw amount every +// period rounds for withdrawWindow after every multiple +// of period. +// +// After expiryRound, all remaining funds in the escrow +// are available to receiver. +// +// Parameters: +// - receiver: address which is authorized to receive withdrawals +// - amount: the maximum number of funds allowed for a single withdrawal +// - withdrawWindow: the duration of a withdrawal period +// - period: the time between a pair of withdrawal periods +// - expiryRound: the round at which the account expires +// - maxFee: maximum fee used by the withdrawal transaction +func MakePeriodicPayment(receiver string, amount, withdrawWindow, period, expiryRound, maxFee uint64) (PeriodicPayment, error) { + leaseBytes := make([]byte, 32) + crypto.RandomBytes(leaseBytes) + leaseString := base64.StdEncoding.EncodeToString(leaseBytes) + return makePeriodicPaymentWithLease(receiver, leaseString, amount, withdrawWindow, period, expiryRound, maxFee) +} + +// makePeriodicPaymentWithLease is as MakePeriodicPayment, but the caller can specify the lease (using b64 string) +func makePeriodicPaymentWithLease(receiver, lease string, amount, withdrawWindow, period, expiryRound, maxFee uint64) (PeriodicPayment, error) { + const referenceProgram = "ASAHAQYFAAQDByYCIAECAwQFBgcIAQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIIJKvkYTkEzwJf2arzJOxERsSogG9nQzKPkpIoc4TzPTFMRAiEjEBIw4QMQIkGCUSEDEEIQQxAggSEDEGKBIQMQkyAxIxBykSEDEIIQUSEDEJKRIxBzIDEhAxAiEGDRAxCCUSEBEQ" + referenceAsBytes, err := base64.StdEncoding.DecodeString(referenceProgram) + if err != nil { + return PeriodicPayment{}, err + } + receiverAddr, err := types.DecodeAddress(receiver) + if err != nil { + return PeriodicPayment{}, err + } + + var referenceOffsets = []uint64{ /*fee*/ 4 /*period*/, 5 /*withdrawWindow*/, 7 /*amount*/, 8 /*expiryRound*/, 9 /*lease*/, 12 /*receiver*/, 46} + injectionVector := []interface{}{maxFee, period, withdrawWindow, amount, expiryRound, lease, receiverAddr} + injectedBytes, err := inject(referenceAsBytes, referenceOffsets, injectionVector) + if err != nil { + return PeriodicPayment{}, err + } + + address := crypto.AddressFromProgram(injectedBytes) + periodicPayment := PeriodicPayment{ + ContractTemplate: ContractTemplate{ + address: address.String(), + program: injectedBytes, + }, + } + return periodicPayment, err +} diff --git a/templates/split.go b/templates/split.go index c6787ce3..8635d990 100644 --- a/templates/split.go +++ b/templates/split.go @@ -3,8 +3,13 @@ package templates import ( "encoding/base64" "fmt" + "math" + + "golang.org/x/crypto/ed25519" + "github.com/algorand/go-algorand-sdk/crypto" - "github.com/algorand/go-algorand-sdk/transaction" + "github.com/algorand/go-algorand-sdk/future" + "github.com/algorand/go-algorand-sdk/logic" "github.com/algorand/go-algorand-sdk/types" ) @@ -17,26 +22,48 @@ type Split struct { receiverTwo types.Address } -//GetSendFundsTransaction returns a group transaction array which transfer funds according to the contract's ratio +//GetSplitFundsTransaction returns a group transaction array which transfer funds according to the contract's ratio // the returned byte array is suitable for passing to SendRawTransaction -// amount: uint64 number of assets to be transferred total -// precise: handles rounding error. When False, the amount will be divided as closely as possible but one account will get -// slightly more. When true, returns an error. -func (contract Split) GetSendFundsTransaction(amount uint64, precise bool, firstRound, lastRound, fee uint64, genesisHash []byte) ([]byte, error) { - ratio := contract.ratn / contract.ratd - amountForReceiverOne := amount * ratio - amountForReceiverTwo := amount * (1 - ratio) - remainder := amount - amountForReceiverOne - amountForReceiverTwo - if precise && remainder != 0 { - return nil, fmt.Errorf("could not precisely divide funds between the two accounts") +// contract: the bytecode of the contract to be used +// amount: uint64 total number of algos to be transferred (payment1_amount + payment2_amount) +// params: is typically received from algod, it defines common-to-all-txns arguments like fee and validity period +func GetSplitFundsTransaction(contract []byte, amount uint64, params types.SuggestedParams) ([]byte, error) { + ints, byteArrays, err := logic.ReadProgram(contract, nil) + if err != nil { + return nil, err + } + ratn := ints[6] + ratd := ints[5] + // Convert the byteArrays[0] to receiver + var receiverOne types.Address //byteArrays[0] + n := copy(receiverOne[:], byteArrays[1]) + if n != ed25519.PublicKeySize { + err = fmt.Errorf("address generated from receiver bytes is the wrong size") + return nil, err + } + // Convert the byteArrays[2] to receiverTwo + var receiverTwo types.Address + n = copy(receiverTwo[:], byteArrays[2]) + if n != ed25519.PublicKeySize { + err = fmt.Errorf("address generated from closeRemainderTo bytes is the wrong size") + return nil, err + } + + ratio := float64(ratd) / float64(ratn) + amountForReceiverOneFloat := float64(amount) / (1 + ratio) + amountForReceiverOne := uint64(math.Round(amountForReceiverOneFloat)) + amountForReceiverTwo := amount - amountForReceiverOne + if ratd*amountForReceiverOne != ratn*amountForReceiverTwo { + err = fmt.Errorf("could not split funds in a way that satisfied the contract ratio (%d * %d != %d * %d)", ratd, amountForReceiverOne, ratn, amountForReceiverTwo) + return nil, err } - from := contract.address - tx1, err := transaction.MakePaymentTxn(from, contract.receiverOne.String(), fee, amountForReceiverOne, firstRound, lastRound, nil, "", "", genesisHash) + from := crypto.AddressFromProgram(contract) + tx1, err := future.MakePaymentTxn(from.String(), receiverOne.String(), amountForReceiverOne, nil, "", params) if err != nil { return nil, err } - tx2, err := transaction.MakePaymentTxn(from, contract.receiverTwo.String(), fee, amountForReceiverTwo, firstRound, lastRound, nil, "", "", genesisHash) + tx2, err := future.MakePaymentTxn(from.String(), receiverTwo.String(), amountForReceiverTwo, nil, "", params) if err != nil { return nil, err } @@ -47,7 +74,7 @@ func (contract Split) GetSendFundsTransaction(amount uint64, precise bool, first tx1.Group = gid tx2.Group = gid - logicSig, err := crypto.MakeLogicSig(contract.program, nil, nil, crypto.MultisigAccount{}) + logicSig, err := crypto.MakeLogicSig(contract, nil, nil, crypto.MultisigAccount{}) if err != nil { return nil, err } @@ -80,14 +107,19 @@ func (contract Split) GetSendFundsTransaction(amount uint64, precise bool, first // // After expiryRound passes, all funds can be refunded to owner. // +// Split ratio: +// firstRecipient_amount * ratd == secondRecipient_amount * ratn +// or phrased another way +// firstRecipient_amount == secondRecipient_amount * (ratn/ratd) +// // Parameters: // - owner: the address to refund funds to on timeout // - receiverOne: the first recipient in the split account // - receiverTwo: the second recipient in the split account -// - ratn: fraction of money to be paid to the first recipient (numerator) -// - ratd: fraction of money to be paid to the first recipient (denominator) +// - ratn: fraction determines resource split ratio (numerator) +// - ratd: fraction determines resource split ratio (denominator) // - expiryRound: the round at which the account expires -// - minPay: minimum amount to be paid out of the account +// - minPay: minimum amount to be paid out of the account to receiverOne // - maxFee: half of the maximum fee used by each split forwarding group transaction func MakeSplit(owner, receiverOne, receiverTwo string, ratn, ratd, expiryRound, minPay, maxFee uint64) (Split, error) { const referenceProgram = "ASAIAQUCAAYHCAkmAyCztwQn0+DycN+vsk+vJWcsoz/b7NDS6i33HOkvTpf+YiC3qUpIgHGWE8/1LPh9SGCalSN7IaITeeWSXbfsS5wsXyC4kBQ38Z8zcwWVAym4S8vpFB/c0XC6R4mnPi9EBADsPDEQIhIxASMMEDIEJBJAABkxCSgSMQcyAxIQMQglEhAxAiEEDRAiQAAuMwAAMwEAEjEJMgMSEDMABykSEDMBByoSEDMACCEFCzMBCCEGCxIQMwAIIQcPEBA=" @@ -95,8 +127,7 @@ func MakeSplit(owner, receiverOne, receiverTwo string, ratn, ratd, expiryRound, if err != nil { return Split{}, err } - - var referenceOffsets = []uint64{ /*fee*/ 4 /*timeout*/, 7 /*ratn*/, 8 /*ratd*/, 9 /*minPay*/, 10 /*owner*/, 14 /*receiver1*/, 47 /*receiver2*/, 80} + var referenceOffsets = []uint64{ /*fee*/ 4 /*timeout*/, 7 /*ratd*/, 8 /*ratn*/, 9 /*minPay*/, 10 /*owner*/, 14 /*receiver1*/, 47 /*receiver2*/, 80} ownerAddr, err := types.DecodeAddress(owner) if err != nil { return Split{}, err @@ -109,7 +140,7 @@ func MakeSplit(owner, receiverOne, receiverTwo string, ratn, ratd, expiryRound, if err != nil { return Split{}, err } - injectionVector := []interface{}{maxFee, expiryRound, ratn, ratd, minPay, ownerAddr, receiverOneAddr, receiverTwoAddr} + injectionVector := []interface{}{maxFee, expiryRound, ratd, ratn, minPay, ownerAddr, receiverOneAddr, receiverTwoAddr} injectedBytes, err := inject(referenceAsBytes, referenceOffsets, injectionVector) if err != nil { return Split{}, err diff --git a/templates/templates_test.go b/templates/templates_test.go index 81157691..e3b1f139 100644 --- a/templates/templates_test.go +++ b/templates/templates_test.go @@ -2,8 +2,13 @@ package templates import ( "encoding/base64" - "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand-sdk/types" "testing" + + "github.com/algorand/go-algorand-sdk/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/future" + + "github.com/stretchr/testify/require" ) func TestSplit(t *testing.T) { @@ -17,10 +22,22 @@ func TestSplit(t *testing.T) { c, err := MakeSplit(owner, receivers[0], receivers[1], ratn, ratd, expiryRound, minPay, maxFee) // Outputs require.NoError(t, err) - goldenProgram := "ASAIAcCWsQICAMDEBx5kkE4mAyCztwQn0+DycN+vsk+vJWcsoz/b7NDS6i33HOkvTpf+YiC3qUpIgHGWE8/1LPh9SGCalSN7IaITeeWSXbfsS5wsXyC4kBQ38Z8zcwWVAym4S8vpFB/c0XC6R4mnPi9EBADsPDEQIhIxASMMEDIEJBJAABkxCSgSMQcyAxIQMQglEhAxAiEEDRAiQAAuMwAAMwEAEjEJMgMSEDMABykSEDMBByoSEDMACCEFCzMBCCEGCxIQMwAIIQcPEBA=" + goldenProgram := "ASAIAcCWsQICAMDEB2QekE4mAyCztwQn0+DycN+vsk+vJWcsoz/b7NDS6i33HOkvTpf+YiC3qUpIgHGWE8/1LPh9SGCalSN7IaITeeWSXbfsS5wsXyC4kBQ38Z8zcwWVAym4S8vpFB/c0XC6R4mnPi9EBADsPDEQIhIxASMMEDIEJBJAABkxCSgSMQcyAxIQMQglEhAxAiEEDRAiQAAuMwAAMwEAEjEJMgMSEDMABykSEDMBByoSEDMACCEFCzMBCCEGCxIQMwAIIQcPEBA=" require.Equal(t, goldenProgram, base64.StdEncoding.EncodeToString(c.GetProgram())) - goldenAddress := "KPYGWKTV7CKMPMTLQRNGMEQRSYTYDHUOFNV4UDSBDLC44CLIJPQWRTCPBU" + goldenAddress := "HDY7A4VHBWQWQZJBEMASFOUZKBNGWBMJEMUXAGZ4SPIRQ6C24MJHUZKFGY" require.Equal(t, goldenAddress, c.GetAddress()) + goldenGenesisHash := "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=" + genesisBytes, _ := base64.StdEncoding.DecodeString(goldenGenesisHash) + goldenStx := "gqRsc2lngaFsxM4BIAgBwJaxAgIAwMQHZB6QTiYDILO3BCfT4PJw36+yT68lZyyjP9vs0NLqLfcc6S9Ol/5iILepSkiAcZYTz/Us+H1IYJqVI3shohN55ZJdt+xLnCxfILiQFDfxnzNzBZUDKbhLy+kUH9zRcLpHiac+L0QEAOw8MRAiEjEBIwwQMgQkEkAAGTEJKBIxBzIDEhAxCCUSEDECIQQNECJAAC4zAAAzAQASMQkyAxIQMwAHKRIQMwEHKhIQMwAIIQULMwEIIQYLEhAzAAghBw8QEKN0eG6Jo2FtdM4ABJPgo2ZlZc4AId/gomZ2AaJnaMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmjZ3JwxCBLA74bTV35FJNL1h0K9ZbRU24b4M1JRkD1YTogvvDXbqJsdmSjcmN2xCC3qUpIgHGWE8/1LPh9SGCalSN7IaITeeWSXbfsS5wsX6NzbmTEIDjx8HKnDaFoZSEjASK6mVBaawWJIylwGzyT0Rh4WuMSpHR5cGWjcGF5gqRsc2lngaFsxM4BIAgBwJaxAgIAwMQHZB6QTiYDILO3BCfT4PJw36+yT68lZyyjP9vs0NLqLfcc6S9Ol/5iILepSkiAcZYTz/Us+H1IYJqVI3shohN55ZJdt+xLnCxfILiQFDfxnzNzBZUDKbhLy+kUH9zRcLpHiac+L0QEAOw8MRAiEjEBIwwQMgQkEkAAGTEJKBIxBzIDEhAxCCUSEDECIQQNECJAAC4zAAAzAQASMQkyAxIQMwAHKRIQMwEHKhIQMwAIIQULMwEIIQYLEhAzAAghBw8QEKN0eG6Jo2FtdM4AD0JAo2ZlZc4AId/gomZ2AaJnaMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmjZ3JwxCBLA74bTV35FJNL1h0K9ZbRU24b4M1JRkD1YTogvvDXbqJsdmSjcmN2xCC4kBQ38Z8zcwWVAym4S8vpFB/c0XC6R4mnPi9EBADsPKNzbmTEIDjx8HKnDaFoZSEjASK6mVBaawWJIylwGzyT0Rh4WuMSpHR5cGWjcGF5" + params := types.SuggestedParams{ + Fee: 10000, + FirstRoundValid: 1, + LastRoundValid: 100, + GenesisHash: genesisBytes, + } + stx, err := GetSplitFundsTransaction(c.GetProgram(), minPay*(ratd+ratn), params) + require.NoError(t, err) + require.Equal(t, goldenStx, base64.StdEncoding.EncodeToString(stx)) } func TestHTLC(t *testing.T) { @@ -28,16 +45,94 @@ func TestHTLC(t *testing.T) { owner := "726KBOYUJJNE5J5UHCSGQGWIBZWKCBN4WYD7YVSTEXEVNFPWUIJ7TAEOPM" receiver := "42NJMHTPFVPXVSDGA6JGKUV6TARV5UZTMPFIREMLXHETRKIVW34QFSDFRE" hashFn := "sha256" - hashImg := "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=" + hashImg := "EHZhE08h/HwCIj1Qq56zYAvD/8NxJCOh5Hux+anb9V8=" expiryRound := uint64(600000) maxFee := uint64(1000) c, err := MakeHTLC(owner, receiver, hashFn, hashImg, expiryRound, maxFee) // Outputs require.NoError(t, err) - goldenProgram := "ASAE6AcBAMDPJCYDIOaalh5vLV96yGYHkmVSvpgjXtMzY8qIkYu5yTipFbb5IH+DsWV/8fxTuS3BgUih1l38LUsfo9Z3KErd0gASbZBpIP68oLsUSlpOp7Q4pGgayA5soQW8tgf8VlMlyVaV9qITMQEiDjEQIxIQMQcyAxIQMQgkEhAxCSgSLQEpEhAxCSoSMQIlDRAREA==" + goldenProgram := "ASAE6AcBAMDPJCYDIOaalh5vLV96yGYHkmVSvpgjXtMzY8qIkYu5yTipFbb5IBB2YRNPIfx8AiI9UKues2ALw//DcSQjoeR7sfmp2/VfIP68oLsUSlpOp7Q4pGgayA5soQW8tgf8VlMlyVaV9qITMQEiDjEQIxIQMQcyAxIQMQgkEhAxCSgSLQEpEhAxCSoSMQIlDRAREA==" require.Equal(t, goldenProgram, base64.StdEncoding.EncodeToString(c.GetProgram())) - goldenAddress := "KNBD7ATNUVQ4NTLOI72EEUWBVMBNKMPHWVBCETERV2W7T2YO6CVMLJRBM4" + goldenAddress := "FBZIR3RWVT2BTGVOG25H3VAOLVD54RTCRNRLQCCJJO6SVSCT5IVDYKNCSU" + require.Equal(t, goldenAddress, c.GetAddress()) + genesisHash := "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=" + genesisBytes, _ := base64.StdEncoding.DecodeString(genesisHash) + params := types.SuggestedParams{ + Fee: 0, + FirstRoundValid: 1, + LastRoundValid: 100, + GenesisID: "", + GenesisHash: genesisBytes, + } + txn, err := future.MakePaymentTxn(goldenAddress, receiver, 0, nil, receiver, params) + require.NoError(t, err) + preImageAsBase64 := "cHJlaW1hZ2U=" + _, stx, err := SignTransactionWithHTLCUnlock(c.GetProgram(), txn, preImageAsBase64) + require.NoError(t, err) + goldenStx := "gqRsc2lngqNhcmeRxAhwcmVpbWFnZaFsxJcBIAToBwEAwM8kJgMg5pqWHm8tX3rIZgeSZVK+mCNe0zNjyoiRi7nJOKkVtvkgEHZhE08h/HwCIj1Qq56zYAvD/8NxJCOh5Hux+anb9V8g/ryguxRKWk6ntDikaBrIDmyhBby2B/xWUyXJVpX2ohMxASIOMRAjEhAxBzIDEhAxCCQSEDEJKBItASkSEDEJKhIxAiUNEBEQo3R4boelY2xvc2XEIOaalh5vLV96yGYHkmVSvpgjXtMzY8qIkYu5yTipFbb5o2ZlZc0D6KJmdgGiZ2jEIH+DsWV/8fxTuS3BgUih1l38LUsfo9Z3KErd0gASbZBpomx2ZKNzbmTEIChyiO42rPQZmq42un3UDl1H3kZii2K4CElLvSrIU+oqpHR5cGWjcGF5" + require.Equal(t, goldenStx, base64.StdEncoding.EncodeToString(stx)) +} + +func TestPeriodicPayment(t *testing.T) { + // Inputs + receiver := "SKXZDBHECM6AS73GVPGJHMIRDMJKEAN5TUGMUPSKJCQ44E6M6TC2H2UJ3I" + artificialLease := "AQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIAQIDBAUGBwg=" + amount := uint64(500000) + withdrawalWindow := uint64(95) + period := uint64(100) + maxFee := uint64(1000) + expiryRound := uint64(2445756) + c, err := makePeriodicPaymentWithLease(receiver, artificialLease, amount, withdrawalWindow, period, expiryRound, maxFee) + // Outputs + require.NoError(t, err) + goldenProgram := "ASAHAegHZABfoMIevKOVASYCIAECAwQFBgcIAQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIIJKvkYTkEzwJf2arzJOxERsSogG9nQzKPkpIoc4TzPTFMRAiEjEBIw4QMQIkGCUSEDEEIQQxAggSEDEGKBIQMQkyAxIxBykSEDEIIQUSEDEJKRIxBzIDEhAxAiEGDRAxCCUSEBEQ" + contractBytes := c.GetProgram() + require.Equal(t, goldenProgram, base64.StdEncoding.EncodeToString(contractBytes)) + goldenAddress := "JMS3K4LSHPULANJIVQBTEDP5PZK6HHMDQS4OKHIMHUZZ6OILYO3FVQW7IY" + require.Equal(t, goldenAddress, c.GetAddress()) + goldenGenesisHash := "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=" + genesisBytes, err := base64.StdEncoding.DecodeString(goldenGenesisHash) + require.NoError(t, err) + stx, err := GetPeriodicPaymentWithdrawalTransaction(contractBytes, 1200, 0, genesisBytes) + require.NoError(t, err) + goldenStx := "gqRsc2lngaFsxJkBIAcB6AdkAF+gwh68o5UBJgIgAQIDBAUGBwgBAgMEBQYHCAECAwQFBgcIAQIDBAUGBwggkq+RhOQTPAl/ZqvMk7ERGxKiAb2dDMo+SkihzhPM9MUxECISMQEjDhAxAiQYJRIQMQQhBDECCBIQMQYoEhAxCTIDEjEHKRIQMQghBRIQMQkpEjEHMgMSEDECIQYNEDEIJRIQERCjdHhuiaNhbXTOAAehIKNmZWXNA+iiZnbNBLCiZ2jEIH+DsWV/8fxTuS3BgUih1l38LUsfo9Z3KErd0gASbZBpomx2zQUPomx4xCABAgMEBQYHCAECAwQFBgcIAQIDBAUGBwgBAgMEBQYHCKNyY3bEIJKvkYTkEzwJf2arzJOxERsSogG9nQzKPkpIoc4TzPTFo3NuZMQgSyW1cXI76LA1KKwDMg39flXjnYOEuOUdDD0znzkLw7akdHlwZaNwYXk=" + require.Equal(t, goldenStx, base64.StdEncoding.EncodeToString(stx)) +} + +func TestDynamicFee(t *testing.T) { + // Inputs + receiver := "726KBOYUJJNE5J5UHCSGQGWIBZWKCBN4WYD7YVSTEXEVNFPWUIJ7TAEOPM" + amount := uint64(5000) + firstValid := uint64(12345) + lastValid := uint64(12346) + closeRemainder := "42NJMHTPFVPXVSDGA6JGKUV6TARV5UZTMPFIREMLXHETRKIVW34QFSDFRE" + artificialLease := "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=" + c, err := makeDynamicFeeWithLease(receiver, closeRemainder, artificialLease, amount, firstValid, lastValid) + require.NoError(t, err) + goldenGenesisHash := "f4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGk=" + genesisBytes, err := base64.StdEncoding.DecodeString(goldenGenesisHash) + require.NoError(t, err) + contractBytes := c.GetProgram() + require.NoError(t, err) + privateKeyOneB64 := "cv8E0Ln24FSkwDgGeuXKStOTGcze5u8yldpXxgrBxumFPYdMJymqcGoxdDeyuM8t6Kxixfq0PJCyJP71uhYT7w==" + privateKeyOne, err := base64.StdEncoding.DecodeString(privateKeyOneB64) + txn, lsig, err := SignDynamicFee(contractBytes, privateKeyOne, genesisBytes) + require.NoError(t, err) + goldenLsig := "gqFsxLEBIAUCAYgnuWC6YCYDIP68oLsUSlpOp7Q4pGgayA5soQW8tgf8VlMlyVaV9qITIOaalh5vLV96yGYHkmVSvpgjXtMzY8qIkYu5yTipFbb5IH+DsWV/8fxTuS3BgUih1l38LUsfo9Z3KErd0gASbZBpMgQiEjMAECMSEDMABzEAEhAzAAgxARIQMRYjEhAxECMSEDEHKBIQMQkpEhAxCCQSEDECJRIQMQQhBBIQMQYqEhCjc2lnxEAhLNdfdDp9Wbi0YwsEQCpP7TVHbHG7y41F4MoESNW/vL1guS+5Wj4f5V9fmM63/VKTSMFidHOSwm5o+pbV5lYH" + require.Equal(t, goldenLsig, base64.StdEncoding.EncodeToString(msgpack.Encode(lsig))) + goldenTxn := "iqNhbXTNE4ilY2xvc2XEIOaalh5vLV96yGYHkmVSvpgjXtMzY8qIkYu5yTipFbb5o2ZlZc0D6KJmds0wOaJnaMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmibHbNMDqibHjEIH+DsWV/8fxTuS3BgUih1l38LUsfo9Z3KErd0gASbZBpo3JjdsQg/ryguxRKWk6ntDikaBrIDmyhBby2B/xWUyXJVpX2ohOjc25kxCCFPYdMJymqcGoxdDeyuM8t6Kxixfq0PJCyJP71uhYT76R0eXBlo3BheQ==" + require.Equal(t, goldenTxn, base64.StdEncoding.EncodeToString(msgpack.Encode(txn))) + privateKeyTwoB64 := "2qjz96Vj9M6YOqtNlfJUOKac13EHCXyDty94ozCjuwwriI+jzFgStFx9E6kEk1l4+lFsW4Te2PY1KV8kNcccRg==" + privateKeyTwo, err := base64.StdEncoding.DecodeString(privateKeyTwoB64) + stxns, err := GetDynamicFeeTransactions(txn, lsig, privateKeyTwo, 1234) + require.NoError(t, err) + // Outputs + goldenProgram := "ASAFAgGIJ7lgumAmAyD+vKC7FEpaTqe0OKRoGsgObKEFvLYH/FZTJclWlfaiEyDmmpYeby1feshmB5JlUr6YI17TM2PKiJGLuck4qRW2+SB/g7Flf/H8U7ktwYFIodZd/C1LH6PWdyhK3dIAEm2QaTIEIhIzABAjEhAzAAcxABIQMwAIMQESEDEWIxIQMRAjEhAxBygSEDEJKRIQMQgkEhAxAiUSEDEEIQQSEDEGKhIQ" + require.Equal(t, goldenProgram, base64.StdEncoding.EncodeToString(contractBytes)) + goldenAddress := "GCI4WWDIWUFATVPOQ372OZYG52EULPUZKI7Y34MXK3ZJKIBZXHD2H5C5TI" require.Equal(t, goldenAddress, c.GetAddress()) + goldenStxns := "gqNzaWfEQJBNVry9qdpnco+uQzwFicUWHteYUIxwDkdHqY5Qw2Q8Fc2StrQUgN+2k8q4rC0LKrTMJQnE+mLWhZgMMJvq3QCjdHhuiqNhbXTOAAWq6qNmZWXOAATzvqJmds0wOaJnaMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmjZ3JwxCCCVfqhCinRBXKMIq9eSrJQIXZ+7iXUTig91oGd/mZEAqJsds0wOqJseMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmjcmN2xCCFPYdMJymqcGoxdDeyuM8t6Kxixfq0PJCyJP71uhYT76NzbmTEICuIj6PMWBK0XH0TqQSTWXj6UWxbhN7Y9jUpXyQ1xxxGpHR5cGWjcGF5gqRsc2lngqFsxLEBIAUCAYgnuWC6YCYDIP68oLsUSlpOp7Q4pGgayA5soQW8tgf8VlMlyVaV9qITIOaalh5vLV96yGYHkmVSvpgjXtMzY8qIkYu5yTipFbb5IH+DsWV/8fxTuS3BgUih1l38LUsfo9Z3KErd0gASbZBpMgQiEjMAECMSEDMABzEAEhAzAAgxARIQMRYjEhAxECMSEDEHKBIQMQkpEhAxCCQSEDECJRIQMQQhBBIQMQYqEhCjc2lnxEAhLNdfdDp9Wbi0YwsEQCpP7TVHbHG7y41F4MoESNW/vL1guS+5Wj4f5V9fmM63/VKTSMFidHOSwm5o+pbV5lYHo3R4boujYW10zROIpWNsb3NlxCDmmpYeby1feshmB5JlUr6YI17TM2PKiJGLuck4qRW2+aNmZWXOAAWq6qJmds0wOaJnaMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmjZ3JwxCCCVfqhCinRBXKMIq9eSrJQIXZ+7iXUTig91oGd/mZEAqJsds0wOqJseMQgf4OxZX/x/FO5LcGBSKHWXfwtSx+j1ncoSt3SABJtkGmjcmN2xCD+vKC7FEpaTqe0OKRoGsgObKEFvLYH/FZTJclWlfaiE6NzbmTEIIU9h0wnKapwajF0N7K4zy3orGLF+rQ8kLIk/vW6FhPvpHR5cGWjcGF5" + require.Equal(t, goldenStxns, base64.StdEncoding.EncodeToString(stxns)) } func TestLimitOrder(t *testing.T) { diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile new file mode 100644 index 00000000..23ad354b --- /dev/null +++ b/test/docker/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:18.04 + +ENV DEBIAN_FRONTEND noninteractive + +# Basic dependencies +ENV HOME /opt +RUN apt-get update && apt-get install -y apt-utils curl git git-core bsdmainutils + +# Install python dependencies +ENV PYENV_ROOT $HOME/pyenv +ENV PATH $PYENV_ROOT/bin:$PATH +RUN apt-get install -y curl gcc make zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev libssl-dev libffi-dev +RUN git clone https://github.com/pyenv/pyenv.git $HOME/pyenv + +RUN eval "$(pyenv init -)" && \ + pyenv install 3.7.1 && \ + pyenv global 3.7.1 && \ + pip install --upgrade pip && \ + pyenv rehash +ENV PATH=$PYENV_ROOT/shims:$PATH + +# Install Go dependencies +ARG GOLANG_VERSION=1.13.8 +RUN curl https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz -o $HOME/go.tar.gz +RUN tar -xvf $HOME/go.tar.gz -C /usr/local +ENV GOROOT /usr/local/go +ENV GOPATH $HOME/go +ENV PATH $GOROOT/bin:$PATH + +# Install algorand-sdk-testing script dependencies +RUN pip3 install gitpython + +RUN mkdir -p $HOME/go/src/github.com/algorand/go-algorand-sdk +WORKDIR $HOME/go/src/github.com/algorand/go-algorand-sdk +CMD ["/bin/bash", "-c", "GO111MODULE=off && temp/docker/setup.py --algod-config temp/config_future && temp/docker/test.py --algod-config temp/config_future --network-dir /opt/testnetwork"] diff --git a/test/docker/run_docker.sh b/test/docker/run_docker.sh new file mode 100755 index 00000000..8c672544 --- /dev/null +++ b/test/docker/run_docker.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +rm -rf temp +rm -rf test/features +git clone --single-branch --branch templates https://github.com/algorand/algorand-sdk-testing.git temp + +cp test/docker/sdk.py temp/docker +mv temp/features test/features + +docker build -t sdk-testing -f test/docker/Dockerfile "$(pwd)" + +docker run -it \ + -v "$(pwd)":/opt/go/src/github.com/algorand/go-algorand-sdk \ + sdk-testing:latest diff --git a/test/docker/sdk.py b/test/docker/sdk.py new file mode 100644 index 00000000..5b9ff84b --- /dev/null +++ b/test/docker/sdk.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +import subprocess +import sys + +default_dirs = { + 'features_dir': '/opt/go/src/github.com/algorand/go-algorand-sdk/test/features', + 'source': '/opt/go/src/github.com/algorand/go-algorand-sdk', + 'docker': '/opt/go/src/github.com/algorand/go-algorand-sdk/test/docker', + 'test': '/opt/go/src/github.com/algorand/go-algorand-sdk/test' +} + +def setup_sdk(): + """ + Setup go cucumber environment. + """ + subprocess.check_call(['go generate %s/...' % default_dirs['source']], shell=True) + +def test_sdk(): + sys.stdout.flush() + subprocess.check_call(['go test'], shell=True, cwd=default_dirs['test']) \ No newline at end of file diff --git a/test/steps_test.go b/test/steps_test.go new file mode 100644 index 00000000..73d2af7b --- /dev/null +++ b/test/steps_test.go @@ -0,0 +1,1841 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/base32" + "encoding/base64" + "encoding/gob" + "flag" + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + "testing" + "time" + + "path/filepath" + + "github.com/algorand/go-algorand-sdk/auction" + "github.com/algorand/go-algorand-sdk/client/algod" + "github.com/algorand/go-algorand-sdk/client/algod/models" + "github.com/algorand/go-algorand-sdk/client/kmd" + "github.com/algorand/go-algorand-sdk/crypto" + "github.com/algorand/go-algorand-sdk/encoding/msgpack" + "github.com/algorand/go-algorand-sdk/future" + "github.com/algorand/go-algorand-sdk/mnemonic" + "github.com/algorand/go-algorand-sdk/templates" + "github.com/algorand/go-algorand-sdk/types" + "github.com/cucumber/godog" + "github.com/cucumber/godog/colors" +) + +var txn types.Transaction +var stx []byte +var stxKmd []byte +var stxObj types.SignedTxn +var txid string +var account crypto.Account +var note []byte +var fee uint64 +var fv uint64 +var lv uint64 +var to string +var gh []byte +var close string +var amt uint64 +var gen string +var a types.Address +var msig crypto.MultisigAccount +var msigsig types.MultisigSig +var kcl kmd.Client +var acl algod.Client +var walletName string +var walletPswd string +var walletID string +var handle string +var versions []string +var status models.NodeStatus +var statusAfter models.NodeStatus +var msigExp kmd.ExportMultisigResponse +var pk string +var accounts []string +var e bool +var lastRound uint64 +var sugParams types.SuggestedParams +var sugFee models.TransactionFee +var bid types.Bid +var sbid types.NoteField +var oldBid types.NoteField +var oldPk string +var newMn string +var mdk types.MasterDerivationKey +var microalgos types.MicroAlgos +var bytetxs [][]byte +var votekey string +var selkey string +var votefst uint64 +var votelst uint64 +var votekd uint64 +var num string +var backupTxnSender string +var groupTxnBytes []byte + +var assetTestFixture struct { + Creator string + AssetIndex uint64 + AssetName string + AssetUnitName string + AssetURL string + AssetMetadataHash string + ExpectedParams models.AssetParams + QueriedParams models.AssetParams + LastTransactionIssued types.Transaction +} + +var contractTestFixture struct { + activeAddress string + contractFundAmount uint64 + split templates.Split + splitN uint64 + splitD uint64 + splitMin uint64 + htlc templates.HTLC + htlcPreImage string + periodicPay templates.PeriodicPayment + periodicPayPeriod uint64 + limitOrder templates.LimitOrder + limitOrderN uint64 + limitOrderD uint64 + limitOrderMin uint64 + dynamicFee templates.DynamicFee +} + +var opt = godog.Options{ + Output: colors.Colored(os.Stdout), + Format: "progress", // can define default values +} + +func init() { + godog.BindFlags("godog.", flag.CommandLine, &opt) +} + +func TestMain(m *testing.M) { + flag.Parse() + opt.Paths = flag.Args() + + status := godog.RunWithOptions("godogs", func(s *godog.Suite) { + FeatureContext(s) + }, opt) + + if st := m.Run(); st > status { + status = st + } + os.Exit(status) +} + +func FeatureContext(s *godog.Suite) { + s.Step("I create a wallet", createWallet) + s.Step("the wallet should exist", walletExist) + s.Step("I get the wallet handle", getHandle) + s.Step("I can get the master derivation key", getMdk) + s.Step("I rename the wallet", renameWallet) + s.Step("I can still get the wallet information with the same handle", getWalletInfo) + s.Step("I renew the wallet handle", renewHandle) + s.Step("I release the wallet handle", releaseHandle) + s.Step("the wallet handle should not work", tryHandle) + s.Step(`payment transaction parameters (\d+) (\d+) (\d+) "([^"]*)" "([^"]*)" "([^"]*)" (\d+) "([^"]*)" "([^"]*)"`, txnParams) + s.Step(`mnemonic for private key "([^"]*)"`, mnForSk) + s.Step("I create the payment transaction", createTxn) + s.Step(`multisig addresses "([^"]*)"`, msigAddresses) + s.Step("I create the multisig payment transaction", createMsigTxn) + s.Step("I sign the multisig transaction with the private key", signMsigTxn) + s.Step("I sign the transaction with the private key", signTxn) + s.Step(`the signed transaction should equal the golden "([^"]*)"`, equalGolden) + s.Step(`the multisig transaction should equal the golden "([^"]*)"`, equalMsigGolden) + s.Step(`the multisig address should equal the golden "([^"]*)"`, equalMsigAddrGolden) + s.Step("I get versions with algod", aclV) + s.Step("v1 should be in the versions", v1InVersions) + s.Step("I get versions with kmd", kclV) + s.Step("I get the status", getStatus) + s.Step(`^I get status after this block`, statusAfterBlock) + s.Step("I can get the block info", block) + s.Step("I import the multisig", importMsig) + s.Step("the multisig should be in the wallet", msigInWallet) + s.Step("I export the multisig", expMsig) + s.Step("the multisig should equal the exported multisig", msigEq) + s.Step("I delete the multisig", deleteMsig) + s.Step("the multisig should not be in the wallet", msigNotInWallet) + s.Step("I generate a key using kmd", genKeyKmd) + s.Step("the key should be in the wallet", keyInWallet) + s.Step("I delete the key", deleteKey) + s.Step("the key should not be in the wallet", keyNotInWallet) + s.Step("I generate a key", genKey) + s.Step("I import the key", importKey) + s.Step("the private key should be equal to the exported private key", skEqExport) + s.Step("a kmd client", kmdClient) + s.Step("an algod client", algodClient) + s.Step("wallet information", walletInfo) + s.Step(`default transaction with parameters (\d+) "([^"]*)"`, defaultTxn) + s.Step(`default multisig transaction with parameters (\d+) "([^"]*)"`, defaultMsigTxn) + s.Step("I get the private key", getSk) + s.Step("I send the transaction", sendTxn) + s.Step("I send the kmd-signed transaction", sendTxnKmd) + s.Step("I send the bogus kmd-signed transaction", sendTxnKmdFailureExpected) + s.Step("I send the multisig transaction", sendMsigTxn) + s.Step("the transaction should go through", checkTxn) + s.Step("the transaction should not go through", txnFail) + s.Step("I sign the transaction with kmd", signKmd) + s.Step("the signed transaction should equal the kmd signed transaction", signBothEqual) + s.Step("I sign the multisig transaction with kmd", signMsigKmd) + s.Step("the multisig transaction should equal the kmd signed multisig transaction", signMsigBothEqual) + s.Step(`I read a transaction "([^"]*)" from file "([^"]*)"`, readTxn) + s.Step("I write the transaction to file", writeTxn) + s.Step("the transaction should still be the same", checkEnc) + s.Step("I do my part", createSaveTxn) + s.Step(`^the node should be healthy`, nodeHealth) + s.Step(`^I get the ledger supply`, ledger) + s.Step(`^I get transactions by address and round`, txnsByAddrRound) + s.Step(`^I get pending transactions`, txnsPending) + s.Step(`^I get the suggested params`, suggestedParams) + s.Step(`^I get the suggested fee`, suggestedFee) + s.Step(`^the fee in the suggested params should equal the suggested fee`, checkSuggested) + s.Step(`^I create a bid`, createBid) + s.Step(`^I encode and decode the bid`, encDecBid) + s.Step(`^the bid should still be the same`, checkBid) + s.Step(`^I decode the address`, decAddr) + s.Step(`^I encode the address`, encAddr) + s.Step(`^the address should still be the same`, checkAddr) + s.Step(`^I convert the private key back to a mnemonic`, skToMn) + s.Step(`^the mnemonic should still be the same as "([^"]*)"`, checkMn) + s.Step(`^mnemonic for master derivation key "([^"]*)"`, mnToMdk) + s.Step(`^I convert the master derivation key back to a mnemonic`, mdkToMn) + s.Step(`^I create the flat fee payment transaction`, createTxnFlat) + s.Step(`^encoded multisig transaction "([^"]*)"`, encMsigTxn) + s.Step(`^I append a signature to the multisig transaction`, appendMsig) + s.Step(`^encoded multisig transactions "([^"]*)"`, encMtxs) + s.Step(`^I merge the multisig transactions`, mergeMsig) + s.Step(`^I convert (\d+) microalgos to algos and back`, microToAlgos) + s.Step(`^it should still be the same amount of microalgos (\d+)`, checkAlgos) + s.Step(`I get account information`, accInfo) + s.Step("I sign the bid", signBid) + s.Step("I get transactions by address only", txnsByAddrOnly) + s.Step("I get transactions by address and date", txnsByAddrDate) + s.Step(`key registration transaction parameters (\d+) (\d+) (\d+) "([^"]*)" "([^"]*)" "([^"]*)" (\d+) (\d+) (\d+) "([^"]*)" "([^"]*)`, keyregTxnParams) + s.Step("I create the key registration transaction", createKeyregTxn) + s.Step(`^I get recent transactions, limited by (\d+) transactions$`, getTxnsByCount) + s.Step(`^I can get account information`, newAccInfo) + s.Step(`^I can get the transaction by ID$`, txnbyID) + s.Step("asset test fixture", createAssetTestFixture) + s.Step(`^default asset creation transaction with total issuance (\d+)$`, defaultAssetCreateTxn) + s.Step(`^I update the asset index$`, getAssetIndex) + s.Step(`^I get the asset info$`, getAssetInfo) + s.Step(`^I should be unable to get the asset info`, failToGetAssetInfo) + s.Step(`^the asset info should match the expected asset info$`, checkExpectedVsActualAssetParams) + s.Step(`^I create a no-managers asset reconfigure transaction$`, createNoManagerAssetReconfigure) + s.Step(`^I create an asset destroy transaction$`, createAssetDestroy) + s.Step(`^I create a transaction for a second account, signalling asset acceptance$`, createAssetAcceptanceForSecondAccount) + s.Step(`^I create a transaction transferring (\d+) assets from creator to a second account$`, createAssetTransferTransactionToSecondAccount) + s.Step(`^the creator should have (\d+) assets remaining$`, theCreatorShouldHaveAssetsRemaining) + s.Step(`^I create a freeze transaction targeting the second account$`, createFreezeTransactionTargetingSecondAccount) + s.Step(`^I create a transaction transferring (\d+) assets from a second account to creator$`, createAssetTransferTransactionFromSecondAccountToCreator) + s.Step(`^I create an un-freeze transaction targeting the second account$`, createUnfreezeTransactionTargetingSecondAccount) + s.Step(`^default-frozen asset creation transaction with total issuance (\d+)$`, defaultAssetCreateTxnWithDefaultFrozen) + s.Step(`^I create a transaction revoking (\d+) assets from a second account to creator$`, createRevocationTransaction) + s.Step(`^a split contract with ratio (\d+) to (\d+) and minimum payment (\d+)$`, aSplitContractWithRatioToAndMinimumPayment) + s.Step(`^I send the split transactions$`, iSendTheSplitTransactions) + s.Step(`^an HTLC contract with hash preimage "([^"]*)"$`, anHTLCContractWithHashPreimage) + s.Step(`^I fund the contract account$`, iFundTheContractAccount) + s.Step(`^I claim the algos$`, iClaimTheAlgosHTLC) + s.Step(`^a periodic payment contract with withdrawing window (\d+) and period (\d+)$`, aPeriodicPaymentContractWithWithdrawingWindowAndPeriod) + s.Step(`^I claim the periodic payment$`, iClaimThePeriodicPayment) + s.Step(`^a limit order contract with parameters (\d+) (\d+) (\d+)$`, aLimitOrderContractWithParameters) + s.Step(`^I swap assets for algos$`, iSwapAssetsForAlgos) + s.Step(`^a dynamic fee contract with amount (\d+)$`, aDynamicFeeContractWithAmount) + s.Step(`^I send the dynamic fee transactions$`, iSendTheDynamicFeeTransaction) + s.Step("contract test fixture", createContractTestFixture) + s.Step(`^I create a transaction transferring assets from creator to a second account$`, iCreateATransactionTransferringAmountAssetsFromCreatorToASecondAccount) // provide handler for when godog misreads + + s.BeforeScenario(func(interface{}) { + stxObj = types.SignedTxn{} + kcl.RenewWalletHandle(handle) + }) +} + +func createWallet() error { + walletName = "Walletgo" + walletPswd = "" + resp, err := kcl.CreateWallet(walletName, walletPswd, "sqlite", types.MasterDerivationKey{}) + if err != nil { + return err + } + walletID = resp.Wallet.ID + return nil +} + +func walletExist() error { + wallets, err := kcl.ListWallets() + if err != nil { + return err + } + for _, w := range wallets.Wallets { + if w.Name == walletName { + return nil + } + } + return fmt.Errorf("Wallet not found") +} + +func getHandle() error { + h, err := kcl.InitWalletHandle(walletID, walletPswd) + if err != nil { + return err + } + handle = h.WalletHandleToken + return nil +} + +func getMdk() error { + _, err := kcl.ExportMasterDerivationKey(handle, walletPswd) + return err +} + +func renameWallet() error { + walletName = "Walletgo_new" + _, err := kcl.RenameWallet(walletID, walletPswd, walletName) + return err +} + +func getWalletInfo() error { + resp, err := kcl.GetWallet(handle) + if resp.WalletHandle.Wallet.Name != walletName { + return fmt.Errorf("Wallet name not equal") + } + return err +} + +func renewHandle() error { + _, err := kcl.RenewWalletHandle(handle) + return err +} + +func releaseHandle() error { + _, err := kcl.ReleaseWalletHandle(handle) + return err +} + +func tryHandle() error { + _, err := kcl.RenewWalletHandle(handle) + if err == nil { + return fmt.Errorf("should be an error; handle was released") + } + return nil +} + +func txnParams(ifee, ifv, ilv int, igh, ito, iclose string, iamt int, igen, inote string) error { + var err error + if inote != "none" { + note, err = base64.StdEncoding.DecodeString(inote) + if err != nil { + return err + } + } else { + note, err = base64.StdEncoding.DecodeString("") + if err != nil { + return err + } + } + gh, err = base64.StdEncoding.DecodeString(igh) + if err != nil { + return err + } + to = ito + fee = uint64(ifee) + fv = uint64(ifv) + lv = uint64(ilv) + if iclose != "none" { + close = iclose + } else { + close = "" + } + amt = uint64(iamt) + if igen != "none" { + gen = igen + } else { + gen = "" + } + if err != nil { + return err + } + return nil +} + +func mnForSk(mn string) error { + sk, err := mnemonic.ToPrivateKey(mn) + if err != nil { + return err + } + account.PrivateKey = sk + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err = enc.Encode(sk.Public()) + if err != nil { + return err + } + addr := buf.Bytes()[4:] + + n := copy(a[:], addr) + if n != 32 { + return fmt.Errorf("wrong address bytes length") + } + return err +} + +func createTxn() error { + var err error + paramsToUse := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisID: gen, + GenesisHash: gh, + FirstRoundValid: types.Round(fv), + LastRoundValid: types.Round(lv), + FlatFee: false, + } + txn, err = future.MakePaymentTxn(a.String(), to, amt, note, close, paramsToUse) + if err != nil { + return err + } + return err +} + +func msigAddresses(addresses string) error { + var err error + addrlist := strings.Fields(addresses) + + var addrStructs []types.Address + for _, a := range addrlist { + addr, err := types.DecodeAddress(a) + if err != nil { + return err + } + + addrStructs = append(addrStructs, addr) + } + msig, err = crypto.MultisigAccountWithParams(1, 2, addrStructs) + + return err +} + +func createMsigTxn() error { + var err error + paramsToUse := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisID: gen, + GenesisHash: gh, + FirstRoundValid: types.Round(fv), + LastRoundValid: types.Round(lv), + FlatFee: false, + } + msigaddr, _ := msig.Address() + txn, err = future.MakePaymentTxn(msigaddr.String(), to, amt, note, close, paramsToUse) + if err != nil { + return err + } + return err + +} + +func signMsigTxn() error { + var err error + txid, stx, err = crypto.SignMultisigTransaction(account.PrivateKey, msig, txn) + + return err +} + +func signTxn() error { + var err error + txid, stx, err = crypto.SignTransaction(account.PrivateKey, txn) + if err != nil { + return err + } + return nil +} + +func equalGolden(golden string) error { + goldenDecoded, err := base64.StdEncoding.DecodeString(golden) + if err != nil { + return err + } + + if !bytes.Equal(goldenDecoded, stx) { + return fmt.Errorf(base64.StdEncoding.EncodeToString(stx)) + } + return nil +} + +func equalMsigAddrGolden(golden string) error { + msigAddr, err := msig.Address() + if err != nil { + return err + } + if golden != msigAddr.String() { + return fmt.Errorf("NOT EQUAL") + } + return nil +} + +func equalMsigGolden(golden string) error { + goldenDecoded, err := base64.StdEncoding.DecodeString(golden) + if err != nil { + return err + } + if !bytes.Equal(goldenDecoded, stx) { + return fmt.Errorf("NOT EQUAL") + } + return nil +} + +func aclV() error { + v, err := acl.Versions() + if err != nil { + return err + } + versions = v.Versions + return nil +} + +func v1InVersions() error { + for _, b := range versions { + if b == "v1" { + return nil + } + } + return fmt.Errorf("v1 not found") +} + +func kclV() error { + v, err := kcl.Version() + versions = v.Versions + return err +} + +func getStatus() error { + var err error + status, err = acl.Status() + lastRound = status.LastRound + return err +} + +func statusAfterBlock() error { + var err error + statusAfter, err = acl.StatusAfterBlock(lastRound) + if err != nil { + return err + } + return nil +} + +func block() error { + _, err := acl.Block(status.LastRound) + return err +} + +func importMsig() error { + _, err := kcl.ImportMultisig(handle, msig.Version, msig.Threshold, msig.Pks) + return err +} + +func msigInWallet() error { + msigs, err := kcl.ListMultisig(handle) + if err != nil { + return err + } + addrs := msigs.Addresses + for _, a := range addrs { + addr, err := msig.Address() + if err != nil { + return err + } + if a == addr.String() { + return nil + } + } + return fmt.Errorf("msig not found") + +} + +func expMsig() error { + addr, err := msig.Address() + if err != nil { + return err + } + msigExp, err = kcl.ExportMultisig(handle, walletPswd, addr.String()) + + return err +} + +func msigEq() error { + eq := true + + if (msig.Pks == nil) != (msigExp.PKs == nil) { + eq = false + } + + if len(msig.Pks) != len(msigExp.PKs) { + eq = false + } + + for i := range msig.Pks { + + if !bytes.Equal(msig.Pks[i], msigExp.PKs[i]) { + eq = false + } + } + + if !eq { + return fmt.Errorf("exported msig not equal to original msig") + } + return nil +} + +func deleteMsig() error { + addr, err := msig.Address() + kcl.DeleteMultisig(handle, walletPswd, addr.String()) + return err +} + +func msigNotInWallet() error { + msigs, err := kcl.ListMultisig(handle) + if err != nil { + return err + } + addrs := msigs.Addresses + for _, a := range addrs { + addr, err := msig.Address() + if err != nil { + return err + } + if a == addr.String() { + return fmt.Errorf("msig found unexpectedly; should have been deleted") + } + } + return nil + +} + +func genKeyKmd() error { + p, err := kcl.GenerateKey(handle) + if err != nil { + return err + } + pk = p.Address + return nil +} + +func keyInWallet() error { + resp, err := kcl.ListKeys(handle) + if err != nil { + return err + } + for _, a := range resp.Addresses { + if pk == a { + return nil + } + } + return fmt.Errorf("key not found") +} + +func deleteKey() error { + _, err := kcl.DeleteKey(handle, walletPswd, pk) + return err +} + +func keyNotInWallet() error { + resp, err := kcl.ListKeys(handle) + if err != nil { + return err + } + for _, a := range resp.Addresses { + if pk == a { + return fmt.Errorf("key found unexpectedly; should have been deleted") + } + } + return nil +} + +func genKey() error { + account = crypto.GenerateAccount() + a = account.Address + pk = a.String() + return nil +} + +func importKey() error { + _, err := kcl.ImportKey(handle, account.PrivateKey) + return err +} + +func skEqExport() error { + exp, err := kcl.ExportKey(handle, walletPswd, a.String()) + if err != nil { + return err + } + kcl.DeleteKey(handle, walletPswd, a.String()) + if bytes.Equal(exp.PrivateKey.Seed(), account.PrivateKey.Seed()) { + return nil + } + return fmt.Errorf("private keys not equal") +} + +func kmdClient() error { + dataDirPath := os.Getenv("NODE_DIR") + "/" + os.Getenv("KMD_DIR") + "/" + tokenBytes, err := ioutil.ReadFile(dataDirPath + "kmd.token") + if err != nil { + return err + } + kmdToken := strings.TrimSpace(string(tokenBytes)) + addressBytes, err := ioutil.ReadFile(dataDirPath + "kmd.net") + if err != nil { + return err + } + kmdAddress := strings.TrimSpace(string(addressBytes)) + arr := strings.Split(kmdAddress, ":") + kmdAddress = "http://localhost:" + arr[1] + + kcl, err = kmd.MakeClient(kmdAddress, kmdToken) + + return err +} + +func algodClient() error { + dataDirPath := os.Getenv("NODE_DIR") + "/" + tokenBytes, err := ioutil.ReadFile(dataDirPath + "algod.token") + if err != nil { + return err + } + algodToken := strings.TrimSpace(string(tokenBytes)) + + addressBytes, err := ioutil.ReadFile(dataDirPath + "algod.net") + if err != nil { + return err + } + algodAddress := strings.TrimSpace(string(addressBytes)) + arr := strings.Split(algodAddress, ":") + algodAddress = "http://localhost:" + arr[1] + + acl, err = algod.MakeClient(algodAddress, algodToken) + _, err = acl.StatusAfterBlock(1) + return err +} + +func walletInfo() error { + walletName = "unencrypted-default-wallet" + walletPswd = "" + wallets, err := kcl.ListWallets() + if err != nil { + return err + } + for _, w := range wallets.Wallets { + if w.Name == walletName { + walletID = w.ID + } + } + h, err := kcl.InitWalletHandle(walletID, walletPswd) + if err != nil { + return err + } + handle = h.WalletHandleToken + accs, err := kcl.ListKeys(handle) + accounts = accs.Addresses + return err +} + +func defaultTxn(iamt int, inote string) error { + var err error + if inote != "none" { + note, err = base64.StdEncoding.DecodeString(inote) + if err != nil { + return err + } + } else { + note, err = base64.StdEncoding.DecodeString("") + if err != nil { + return err + } + } + + amt = uint64(iamt) + pk = accounts[0] + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + txn, err = future.MakePaymentTxn(accounts[0], accounts[1], amt, note, "", params) + return err +} + +func defaultMsigTxn(iamt int, inote string) error { + var err error + if inote != "none" { + note, err = base64.StdEncoding.DecodeString(inote) + if err != nil { + return err + } + } else { + note, err = base64.StdEncoding.DecodeString("") + if err != nil { + return err + } + } + + amt = uint64(iamt) + pk = accounts[0] + + var addrStructs []types.Address + for _, a := range accounts { + addr, err := types.DecodeAddress(a) + if err != nil { + return err + } + + addrStructs = append(addrStructs, addr) + } + + msig, err = crypto.MultisigAccountWithParams(1, 1, addrStructs) + if err != nil { + return err + } + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + addr, err := msig.Address() + if err != nil { + return err + } + txn, err = future.MakePaymentTxn(addr.String(), accounts[1], amt, note, "", params) + if err != nil { + return err + } + return nil +} + +func getSk() error { + sk, err := kcl.ExportKey(handle, walletPswd, pk) + if err != nil { + return err + } + account.PrivateKey = sk.PrivateKey + return nil +} + +func sendTxn() error { + tx, err := acl.SendRawTransaction(stx) + if err != nil { + return err + } + txid = tx.TxID + return nil +} + +func sendTxnKmd() error { + tx, err := acl.SendRawTransaction(stxKmd) + if err != nil { + e = true + } + txid = tx.TxID + return nil +} + +func sendTxnKmdFailureExpected() error { + tx, err := acl.SendRawTransaction(stxKmd) + if err == nil { + e = false + return fmt.Errorf("expected an error when sending kmd-signed transaction but no error occurred") + } + e = true + txid = tx.TxID + return nil +} + +func sendMsigTxn() error { + _, err := acl.SendRawTransaction(stx) + + if err != nil { + e = true + } + + return nil +} + +func checkTxn() error { + _, err := acl.PendingTransactionInformation(txid) + if err != nil { + return err + } + _, err = acl.StatusAfterBlock(lastRound + 2) + if err != nil { + return err + } + if txn.Sender.String() != "" && txn.Sender.String() != "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" { + _, err = acl.TransactionInformation(txn.Sender.String(), txid) + } else { + _, err = acl.TransactionInformation(backupTxnSender, txid) + } + if err != nil { + return err + } + _, err = acl.TransactionByID(txid) + return err +} + +func txnbyID() error { + var err error + _, err = acl.StatusAfterBlock(lastRound + 2) + if err != nil { + return err + } + _, err = acl.TransactionByID(txid) + return err +} + +func txnFail() error { + if e { + return nil + } + return fmt.Errorf("sending the transaction should have failed") +} + +func signKmd() error { + s, err := kcl.SignTransaction(handle, walletPswd, txn) + if err != nil { + return err + } + stxKmd = s.SignedTransaction + return nil +} + +func signBothEqual() error { + if bytes.Equal(stx, stxKmd) { + return nil + } + return fmt.Errorf("signed transactions not equal") +} + +func signMsigKmd() error { + kcl.ImportMultisig(handle, msig.Version, msig.Threshold, msig.Pks) + decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(pk) + s, err := kcl.MultisigSignTransaction(handle, walletPswd, txn, decoded[:32], types.MultisigSig{}) + if err != nil { + return err + } + msgpack.Decode(s.Multisig, &msigsig) + stxObj.Msig = msigsig + stxObj.Sig = types.Signature{} + stxObj.Txn = txn + stxKmd = msgpack.Encode(stxObj) + return nil +} + +func signMsigBothEqual() error { + addr, err := msig.Address() + if err != nil { + return err + } + kcl.DeleteMultisig(handle, walletPswd, addr.String()) + if bytes.Equal(stx, stxKmd) { + return nil + } + return fmt.Errorf("signed transactions not equal") + +} + +func readTxn(encodedTxn string, inum string) error { + encodedBytes, err := base64.StdEncoding.DecodeString(encodedTxn) + if err != nil { + return err + } + path, err := os.Getwd() + if err != nil { + return err + } + num = inum + path = filepath.Dir(filepath.Dir(path)) + "/temp/old" + num + ".tx" + err = ioutil.WriteFile(path, encodedBytes, 0644) + data, err := ioutil.ReadFile(path) + if err != nil { + return err + } + err = msgpack.Decode(data, &stxObj) + return err +} + +func writeTxn() error { + path, err := os.Getwd() + if err != nil { + return err + } + path = filepath.Dir(filepath.Dir(path)) + "/temp/raw" + num + ".tx" + data := msgpack.Encode(stxObj) + err = ioutil.WriteFile(path, data, 0644) + return err +} + +func checkEnc() error { + path, err := os.Getwd() + if err != nil { + return err + } + pathold := filepath.Dir(filepath.Dir(path)) + "/temp/old" + num + ".tx" + dataold, err := ioutil.ReadFile(pathold) + + pathnew := filepath.Dir(filepath.Dir(path)) + "/temp/raw" + num + ".tx" + datanew, err := ioutil.ReadFile(pathnew) + + if bytes.Equal(dataold, datanew) { + return nil + } + return fmt.Errorf("should be equal") +} + +func createSaveTxn() error { + var err error + + amt = 100000 + pk = accounts[0] + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + txn, err = future.MakePaymentTxn(accounts[0], accounts[1], amt, note, "", params) + if err != nil { + return err + } + + path, err := os.Getwd() + if err != nil { + return err + } + path = filepath.Dir(filepath.Dir(path)) + "/temp/txn.tx" + data := msgpack.Encode(txn) + err = ioutil.WriteFile(path, data, 0644) + return err +} + +func nodeHealth() error { + err := acl.HealthCheck() + return err +} + +func ledger() error { + _, err := acl.LedgerSupply() + return err +} + +func txnsByAddrRound() error { + lr, err := acl.Status() + if err != nil { + return err + } + _, err = acl.TransactionsByAddr(accounts[0], 1, lr.LastRound) + return err +} + +func txnsByAddrOnly() error { + _, err := acl.TransactionsByAddrLimit(accounts[0], 10) + return err +} + +func txnsByAddrDate() error { + fromDate := time.Now().Format("2006-01-02") + _, err := acl.TransactionsByAddrForDate(accounts[0], fromDate, fromDate) + return err +} + +func txnsPending() error { + _, err := acl.GetPendingTransactions(10) + return err +} + +func suggestedParams() error { + var err error + sugParams, err = acl.BuildSuggestedParams() + return err +} + +func suggestedFee() error { + var err error + sugFee, err = acl.SuggestedFee() + return err +} + +func checkSuggested() error { + if uint64(sugParams.Fee) != sugFee.Fee { + return fmt.Errorf("suggested fee from params should be equal to suggested fee") + } + return nil +} + +func createBid() error { + var err error + account = crypto.GenerateAccount() + bid, err = auction.MakeBid(account.Address.String(), 1, 2, 3, account.Address.String(), 4) + return err +} + +func encDecBid() error { + temp := msgpack.Encode(sbid) + err := msgpack.Decode(temp, &sbid) + return err +} + +func signBid() error { + signedBytes, err := crypto.SignBid(account.PrivateKey, bid) + if err != nil { + return err + } + err = msgpack.Decode(signedBytes, &sbid) + if err != nil { + return err + } + err = msgpack.Decode(signedBytes, &oldBid) + return err +} + +func checkBid() error { + if sbid != oldBid { + return fmt.Errorf("bid should still be the same") + } + return nil +} + +func decAddr() error { + var err error + oldPk = pk + a, err = types.DecodeAddress(pk) + return err +} + +func encAddr() error { + pk = a.String() + return nil +} + +func checkAddr() error { + if pk != oldPk { + return fmt.Errorf("A decoded and encoded address should equal the original address") + } + return nil +} + +func skToMn() error { + var err error + newMn, err = mnemonic.FromPrivateKey(account.PrivateKey) + return err +} + +func checkMn(mn string) error { + if mn != newMn { + return fmt.Errorf("the mnemonic should equal the original mnemonic") + } + return nil +} + +func mnToMdk(mn string) error { + var err error + mdk, err = mnemonic.ToMasterDerivationKey(mn) + return err +} + +func mdkToMn() error { + var err error + newMn, err = mnemonic.FromMasterDerivationKey(mdk) + return err +} + +func createTxnFlat() error { + var err error + paramsToUse := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisID: gen, + GenesisHash: gh, + FirstRoundValid: types.Round(fv), + LastRoundValid: types.Round(lv), + FlatFee: true, + } + txn, err = future.MakePaymentTxn(a.String(), to, amt, note, close, paramsToUse) + if err != nil { + return err + } + return err +} + +func encMsigTxn(encoded string) error { + var err error + stx, err = base64.StdEncoding.DecodeString(encoded) + if err != nil { + return err + } + err = msgpack.Decode(stx, &stxObj) + return err +} + +func appendMsig() error { + var err error + msig, err = crypto.MultisigAccountFromSig(stxObj.Msig) + if err != nil { + return err + } + _, stx, err = crypto.AppendMultisigTransaction(account.PrivateKey, msig, stx) + return err +} + +func encMtxs(txs string) error { + var err error + enctxs := strings.Split(txs, " ") + bytetxs = make([][]byte, len(enctxs)) + for i := range enctxs { + bytetxs[i], err = base64.StdEncoding.DecodeString(enctxs[i]) + if err != nil { + return err + } + } + return nil +} + +func mergeMsig() (err error) { + _, stx, err = crypto.MergeMultisigTransactions(bytetxs...) + return +} + +func microToAlgos(ma int) error { + microalgos = types.MicroAlgos(ma) + microalgos = types.ToMicroAlgos(microalgos.ToAlgos()) + return nil +} + +func checkAlgos(ma int) error { + if types.MicroAlgos(ma) != microalgos { + return fmt.Errorf("Converting to and from algos should not change the value") + } + return nil +} + +func accInfo() error { + _, err := acl.AccountInformation(accounts[0]) + return err +} + +func newAccInfo() error { + _, err := acl.AccountInformation(pk) + _, _ = kcl.DeleteKey(handle, walletPswd, pk) + return err +} + +func keyregTxnParams(ifee, ifv, ilv int, igh, ivotekey, iselkey string, ivotefst, ivotelst, ivotekd int, igen, inote string) error { + var err error + if inote != "none" { + note, err = base64.StdEncoding.DecodeString(inote) + if err != nil { + return err + } + } else { + note, err = base64.StdEncoding.DecodeString("") + if err != nil { + return err + } + } + gh, err = base64.StdEncoding.DecodeString(igh) + if err != nil { + return err + } + votekey = ivotekey + selkey = iselkey + fee = uint64(ifee) + fv = uint64(ifv) + lv = uint64(ilv) + votefst = uint64(ivotefst) + votelst = uint64(ivotelst) + votekd = uint64(ivotekd) + if igen != "none" { + gen = igen + } else { + gen = "" + } + if err != nil { + return err + } + return nil +} + +func createKeyregTxn() (err error) { + paramsToUse := types.SuggestedParams{ + Fee: types.MicroAlgos(fee), + GenesisID: gen, + GenesisHash: gh, + FirstRoundValid: types.Round(fv), + LastRoundValid: types.Round(lv), + FlatFee: false, + } + txn, err = future.MakeKeyRegTxn(a.String(), note, paramsToUse, votekey, selkey, votefst, votelst, votekd) + if err != nil { + return err + } + return err +} + +func getTxnsByCount(cnt int) error { + _, err := acl.TransactionsByAddrLimit(accounts[0], uint64(cnt)) + return err +} + +func createAssetTestFixture() error { + assetTestFixture.Creator = "" + assetTestFixture.AssetIndex = 1 + assetTestFixture.AssetName = "testcoin" + assetTestFixture.AssetUnitName = "coins" + assetTestFixture.AssetURL = "http://test" + assetTestFixture.AssetMetadataHash = "fACPO4nRgO55j1ndAK3W6Sgc4APkcyFh" + assetTestFixture.ExpectedParams = models.AssetParams{} + assetTestFixture.QueriedParams = models.AssetParams{} + assetTestFixture.LastTransactionIssued = types.Transaction{} + return nil +} + +func convertTransactionAssetParamsToModelsAssetParam(input types.AssetParams) models.AssetParams { + result := models.AssetParams{ + Total: input.Total, + Decimals: input.Decimals, + DefaultFrozen: input.DefaultFrozen, + ManagerAddr: input.Manager.String(), + ReserveAddr: input.Reserve.String(), + FreezeAddr: input.Freeze.String(), + ClawbackAddr: input.Clawback.String(), + UnitName: input.UnitName, + AssetName: input.AssetName, + URL: input.URL, + MetadataHash: input.MetadataHash[:], + } + // input doesn't have Creator so that will remain empty + return result +} + +func assetCreateTxnHelper(issuance int, frozenState bool) error { + accountToUse := accounts[0] + assetTestFixture.Creator = accountToUse + creator := assetTestFixture.Creator + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + assetIssuance := uint64(issuance) + manager := creator + reserve := creator + freeze := creator + clawback := creator + unitName := assetTestFixture.AssetUnitName + assetName := assetTestFixture.AssetName + url := assetTestFixture.AssetURL + metadataHash := assetTestFixture.AssetMetadataHash + assetCreateTxn, err := future.MakeAssetCreateTxn(creator, assetNote, params, assetIssuance, 0, frozenState, manager, reserve, freeze, clawback, unitName, assetName, url, metadataHash) + assetTestFixture.LastTransactionIssued = assetCreateTxn + txn = assetCreateTxn + assetTestFixture.ExpectedParams = convertTransactionAssetParamsToModelsAssetParam(assetCreateTxn.AssetParams) + //convertTransactionAssetParamsToModelsAssetParam leaves creator blank, repopulate + assetTestFixture.ExpectedParams.Creator = creator + return err +} + +func defaultAssetCreateTxn(issuance int) error { + return assetCreateTxnHelper(issuance, false) +} + +func defaultAssetCreateTxnWithDefaultFrozen(issuance int) error { + return assetCreateTxnHelper(issuance, true) +} + +func createNoManagerAssetReconfigure() error { + creator := assetTestFixture.Creator + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + reserve := "" + freeze := "" + clawback := "" + manager := creator // if this were "" as well, this wouldn't be a reconfigure txn, it would be a destroy txn + assetReconfigureTxn, err := future.MakeAssetConfigTxn(creator, assetNote, params, assetTestFixture.AssetIndex, manager, reserve, freeze, clawback, false) + assetTestFixture.LastTransactionIssued = assetReconfigureTxn + txn = assetReconfigureTxn + // update expected params + assetTestFixture.ExpectedParams.ReserveAddr = reserve + assetTestFixture.ExpectedParams.FreezeAddr = freeze + assetTestFixture.ExpectedParams.ClawbackAddr = clawback + return err +} + +func createAssetDestroy() error { + creator := assetTestFixture.Creator + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + assetDestroyTxn, err := future.MakeAssetDestroyTxn(creator, assetNote, params, assetTestFixture.AssetIndex) + assetTestFixture.LastTransactionIssued = assetDestroyTxn + txn = assetDestroyTxn + // update expected params + assetTestFixture.ExpectedParams.ReserveAddr = "" + assetTestFixture.ExpectedParams.FreezeAddr = "" + assetTestFixture.ExpectedParams.ClawbackAddr = "" + assetTestFixture.ExpectedParams.ManagerAddr = "" + return err +} + +// used in getAssetIndex and similar to get the index of the most recently operated on asset +func getMaxKey(numbers map[uint64]models.AssetParams) uint64 { + var maxNumber uint64 + for n := range numbers { + maxNumber = n + break + } + for n := range numbers { + if n > maxNumber { + maxNumber = n + } + } + return maxNumber +} + +func getAssetIndex() error { + accountResp, err := acl.AccountInformation(assetTestFixture.Creator) + if err != nil { + return err + } + // get most recent asset index + assetTestFixture.AssetIndex = getMaxKey(accountResp.AssetParams) + return nil +} + +func getAssetInfo() error { + response, err := acl.AssetInformation(assetTestFixture.AssetIndex) + assetTestFixture.QueriedParams = response + return err +} + +func failToGetAssetInfo() error { + _, err := acl.AssetInformation(assetTestFixture.AssetIndex) + if err != nil { + return nil + } + return fmt.Errorf("expected an error getting asset with index %v and creator %v, but no error was returned", + assetTestFixture.AssetIndex, assetTestFixture.Creator) +} + +func checkExpectedVsActualAssetParams() error { + expectedParams := assetTestFixture.ExpectedParams + actualParams := assetTestFixture.QueriedParams + nameMatch := expectedParams.AssetName == actualParams.AssetName + if !nameMatch { + return fmt.Errorf("expected asset name was %v but actual asset name was %v", + expectedParams.AssetName, actualParams.AssetName) + } + unitMatch := expectedParams.UnitName == actualParams.UnitName + if !unitMatch { + return fmt.Errorf("expected unit name was %v but actual unit name was %v", + expectedParams.UnitName, actualParams.UnitName) + } + urlMatch := expectedParams.URL == actualParams.URL + if !urlMatch { + return fmt.Errorf("expected URL was %v but actual URL was %v", + expectedParams.URL, actualParams.URL) + } + hashMatch := reflect.DeepEqual(expectedParams.MetadataHash, actualParams.MetadataHash) + if !hashMatch { + return fmt.Errorf("expected MetadataHash was %v but actual MetadataHash was %v", + expectedParams.MetadataHash, actualParams.MetadataHash) + } + issuanceMatch := expectedParams.Total == actualParams.Total + if !issuanceMatch { + return fmt.Errorf("expected total issuance was %v but actual issuance was %v", + expectedParams.Total, actualParams.Total) + } + defaultFrozenMatch := expectedParams.DefaultFrozen == actualParams.DefaultFrozen + if !defaultFrozenMatch { + return fmt.Errorf("expected default frozen state %v but actual default frozen state was %v", + expectedParams.DefaultFrozen, actualParams.DefaultFrozen) + } + managerMatch := expectedParams.ManagerAddr == actualParams.ManagerAddr + if !managerMatch { + return fmt.Errorf("expected asset manager was %v but actual asset manager was %v", + expectedParams.ManagerAddr, actualParams.ManagerAddr) + } + reserveMatch := expectedParams.ReserveAddr == actualParams.ReserveAddr + if !reserveMatch { + return fmt.Errorf("expected asset reserve was %v but actual asset reserve was %v", + expectedParams.ReserveAddr, actualParams.ReserveAddr) + } + freezeMatch := expectedParams.FreezeAddr == actualParams.FreezeAddr + if !freezeMatch { + return fmt.Errorf("expected freeze manager was %v but actual freeze manager was %v", + expectedParams.FreezeAddr, actualParams.FreezeAddr) + } + clawbackMatch := expectedParams.ClawbackAddr == actualParams.ClawbackAddr + if !clawbackMatch { + return fmt.Errorf("expected revocation (clawback) manager was %v but actual revocation manager was %v", + expectedParams.ClawbackAddr, actualParams.ClawbackAddr) + } + return nil +} + +func theCreatorShouldHaveAssetsRemaining(expectedBal int) error { + expectedBalance := uint64(expectedBal) + accountResp, err := acl.AccountInformation(assetTestFixture.Creator) + if err != nil { + return err + } + holding, ok := accountResp.Assets[assetTestFixture.AssetIndex] + if !ok { + return fmt.Errorf("attempted to get balance of account %v for creator %v and index %v, but no balance was found for that index", assetTestFixture.Creator, assetTestFixture.Creator, assetTestFixture.AssetIndex) + } + if holding.Amount != expectedBalance { + return fmt.Errorf("actual balance %v differed from expected balance %v", holding.Amount, expectedBalance) + } + return nil +} + +func createAssetAcceptanceForSecondAccount() error { + accountToUse := accounts[1] + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + assetAcceptanceTxn, err := future.MakeAssetAcceptanceTxn(accountToUse, assetNote, params, assetTestFixture.AssetIndex) + assetTestFixture.LastTransactionIssued = assetAcceptanceTxn + txn = assetAcceptanceTxn + return err +} + +func createAssetTransferTransactionToSecondAccount(amount int) error { + recipient := accounts[1] + creator := assetTestFixture.Creator + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + sendAmount := uint64(amount) + closeAssetsTo := "" + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + assetAcceptanceTxn, err := future.MakeAssetTransferTxn(creator, recipient, sendAmount, assetNote, params, closeAssetsTo, assetTestFixture.AssetIndex) + assetTestFixture.LastTransactionIssued = assetAcceptanceTxn + txn = assetAcceptanceTxn + return err +} + +func createAssetTransferTransactionFromSecondAccountToCreator(amount int) error { + recipient := assetTestFixture.Creator + sender := accounts[1] + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + sendAmount := uint64(amount) + closeAssetsTo := "" + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + assetAcceptanceTxn, err := future.MakeAssetTransferTxn(sender, recipient, sendAmount, assetNote, params, closeAssetsTo, assetTestFixture.AssetIndex) + assetTestFixture.LastTransactionIssued = assetAcceptanceTxn + txn = assetAcceptanceTxn + return err +} + +// sets up a freeze transaction, with freeze state `setting` against target account `target` +// assumes creator is asset freeze manager +func freezeTransactionHelper(target string, setting bool) error { + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + assetNote := []byte(nil) + assetFreezeOrUnfreezeTxn, err := future.MakeAssetFreezeTxn(assetTestFixture.Creator, assetNote, params, assetTestFixture.AssetIndex, target, setting) + assetTestFixture.LastTransactionIssued = assetFreezeOrUnfreezeTxn + txn = assetFreezeOrUnfreezeTxn + return err +} + +func createFreezeTransactionTargetingSecondAccount() error { + return freezeTransactionHelper(accounts[1], true) +} + +func createUnfreezeTransactionTargetingSecondAccount() error { + return freezeTransactionHelper(accounts[1], false) +} + +func createRevocationTransaction(amount int) error { + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + revocationAmount := uint64(amount) + assetNote := []byte(nil) + assetRevokeTxn, err := future.MakeAssetRevocationTxn(assetTestFixture.Creator, accounts[1], revocationAmount, assetTestFixture.Creator, assetNote, params, assetTestFixture.AssetIndex) + assetTestFixture.LastTransactionIssued = assetRevokeTxn + txn = assetRevokeTxn + return err +} + +func createContractTestFixture() error { + contractTestFixture.split = templates.Split{} + contractTestFixture.htlc = templates.HTLC{} + contractTestFixture.periodicPay = templates.PeriodicPayment{} + contractTestFixture.limitOrder = templates.LimitOrder{} + contractTestFixture.dynamicFee = templates.DynamicFee{} + contractTestFixture.activeAddress = "" + contractTestFixture.htlcPreImage = "" + contractTestFixture.limitOrderN = 0 + contractTestFixture.limitOrderD = 0 + contractTestFixture.limitOrderMin = 0 + contractTestFixture.splitN = 0 + contractTestFixture.splitD = 0 + contractTestFixture.splitMin = 0 + contractTestFixture.contractFundAmount = 0 + contractTestFixture.periodicPayPeriod = 0 + return nil +} + +func aSplitContractWithRatioToAndMinimumPayment(ratn, ratd, minPay int) error { + owner := accounts[0] + receivers := [2]string{accounts[0], accounts[1]} + expiryRound := uint64(100) + maxFee := uint64(5000000) + contractTestFixture.splitN = uint64(ratn) + contractTestFixture.splitD = uint64(ratd) + contractTestFixture.splitMin = uint64(minPay) + c, err := templates.MakeSplit(owner, receivers[0], receivers[1], uint64(ratn), uint64(ratd), expiryRound, uint64(minPay), maxFee) + contractTestFixture.split = c + contractTestFixture.activeAddress = c.GetAddress() + // add much more than enough to evenly split + contractTestFixture.contractFundAmount = uint64(minPay*(ratd+ratn)) * 10 + return err +} + +func iSendTheSplitTransactions() error { + amount := contractTestFixture.splitMin * (contractTestFixture.splitN + contractTestFixture.splitD) + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + txnBytes, err := templates.GetSplitFundsTransaction(contractTestFixture.split.GetProgram(), amount, params) + if err != nil { + return err + } + txIdResponse, err := acl.SendRawTransaction(txnBytes) + txid = txIdResponse.TxID + // hack to make checkTxn work + backupTxnSender = contractTestFixture.split.GetAddress() + return err +} + +func anHTLCContractWithHashPreimage(preImage string) error { + hashImage := sha256.Sum256([]byte(preImage)) + owner := accounts[0] + receiver := accounts[1] + hashFn := "sha256" + expiryRound := uint64(100) + maxFee := uint64(1000000) + hashB64 := base64.StdEncoding.EncodeToString(hashImage[:]) + c, err := templates.MakeHTLC(owner, receiver, hashFn, hashB64, expiryRound, maxFee) + contractTestFixture.htlc = c + contractTestFixture.htlcPreImage = preImage + contractTestFixture.activeAddress = c.GetAddress() + contractTestFixture.contractFundAmount = 100000000 + return err +} + +func iFundTheContractAccount() error { + // send the requested money to c.address + amount := contractTestFixture.contractFundAmount + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + txn, err = future.MakePaymentTxn(accounts[0], contractTestFixture.activeAddress, amount, nil, "", params) + if err != nil { + return err + } + err = signKmd() + if err != nil { + return err + } + err = sendTxnKmd() + if err != nil { + return err + } + return checkTxn() +} + +// used in HTLC +func iClaimTheAlgosHTLC() error { + preImage := contractTestFixture.htlcPreImage + preImageAsArgument := []byte(preImage) + args := make([][]byte, 1) + args[0] = preImageAsArgument + receiver := accounts[1] // was set as receiver in setup step + var blankMultisig crypto.MultisigAccount + lsig, err := crypto.MakeLogicSig(contractTestFixture.htlc.GetProgram(), args, nil, blankMultisig) + if err != nil { + return err + } + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + txn, err = future.MakePaymentTxn(contractTestFixture.activeAddress, receiver, 0, nil, receiver, params) + if err != nil { + return err + } + txn.Receiver = types.Address{} //txn must have no receiver but MakePayment disallows this. + txid, stx, err = crypto.SignLogicsigTransaction(lsig, txn) + if err != nil { + return err + } + return sendTxn() +} + +func aPeriodicPaymentContractWithWithdrawingWindowAndPeriod(withdrawWindow, period int) error { + receiver := accounts[0] + amount := uint64(10000000) + // add far more than enough to withdraw + contractTestFixture.contractFundAmount = amount * 10 + expiryRound := uint64(100) + maxFee := uint64(1000000000000) + contract, err := templates.MakePeriodicPayment(receiver, amount, uint64(withdrawWindow), uint64(period), expiryRound, maxFee) + contractTestFixture.activeAddress = contract.GetAddress() + contractTestFixture.periodicPay = contract + contractTestFixture.periodicPayPeriod = uint64(period) + return err +} + +func iClaimThePeriodicPayment() error { + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + txnFirstValid := uint64(params.FirstRoundValid) + remainder := txnFirstValid % contractTestFixture.periodicPayPeriod + txnFirstValid += remainder + stx, err = templates.GetPeriodicPaymentWithdrawalTransaction(contractTestFixture.periodicPay.GetProgram(), txnFirstValid, uint64(params.Fee), params.GenesisHash) + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) // used in send/checkTxn + return sendTxn() +} + +func aLimitOrderContractWithParameters(ratn, ratd, minTrade int) error { + maxFee := uint64(100000) + expiryRound := uint64(100) + contractTestFixture.limitOrderN = uint64(ratn) + contractTestFixture.limitOrderD = uint64(ratd) + contractTestFixture.limitOrderMin = uint64(minTrade) + contractTestFixture.contractFundAmount = 2 * uint64(minTrade) + if contractTestFixture.contractFundAmount < 1000000 { + contractTestFixture.contractFundAmount = 1000000 + } + contract, err := templates.MakeLimitOrder(accounts[0], assetTestFixture.AssetIndex, uint64(ratn), uint64(ratd), expiryRound, uint64(minTrade), maxFee) + contractTestFixture.activeAddress = contract.GetAddress() + contractTestFixture.limitOrder = contract + return err +} + +// godog misreads the step for this function, so provide a handler for when it does so +func iCreateATransactionTransferringAmountAssetsFromCreatorToASecondAccount() error { + return createAssetTransferTransactionToSecondAccount(500000) +} + +func iSwapAssetsForAlgos() error { + exp, err := kcl.ExportKey(handle, walletPswd, accounts[1]) + if err != nil { + return err + } + secretKey := exp.PrivateKey + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + contract := contractTestFixture.limitOrder.GetProgram() + microAlgoAmount := contractTestFixture.limitOrderMin + 1 // just over the minimum + assetAmount := microAlgoAmount * contractTestFixture.limitOrderN / contractTestFixture.limitOrderD + assetAmount += 1 // assetAmount initialized to absolute minimum, will fail greater-than check, so increment by one for a better deal + stx, err = contractTestFixture.limitOrder.GetSwapAssetsTransaction(assetAmount, microAlgoAmount, contract, secretKey, params) + if err != nil { + return err + } + // hack to make checktxn work + txn = types.Transaction{} + backupTxnSender = contractTestFixture.limitOrder.GetAddress() // used in checktxn + return sendTxn() +} + +func aDynamicFeeContractWithAmount(amount int) error { + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + txnFirstValid := lastRound + txnLastValid := txnFirstValid + 10 + contractTestFixture.contractFundAmount = uint64(10 * amount) + contract, err := templates.MakeDynamicFee(accounts[1], "", uint64(amount), txnFirstValid, txnLastValid) + + contractTestFixture.dynamicFee = contract + contractTestFixture.activeAddress = contract.GetAddress() + return err +} + +func iSendTheDynamicFeeTransaction() error { + params, err := acl.BuildSuggestedParams() + if err != nil { + return err + } + lastRound = uint64(params.FirstRoundValid) + exp, err := kcl.ExportKey(handle, walletPswd, accounts[0]) + if err != nil { + return err + } + secretKeyOne := exp.PrivateKey + initialTxn, lsig, err := templates.SignDynamicFee(contractTestFixture.dynamicFee.GetProgram(), secretKeyOne, params.GenesisHash) + if err != nil { + return err + } + exp, err = kcl.ExportKey(handle, walletPswd, accounts[1]) + if err != nil { + return err + } + secretKeyTwo := exp.PrivateKey + groupTxnBytes, err := templates.GetDynamicFeeTransactions(initialTxn, lsig, secretKeyTwo, uint64(params.Fee)) + // hack to make checkTxn work + txn = initialTxn + // end hack to make checkTxn work + response, err := acl.SendRawTransaction(groupTxnBytes) + txid = response.TxID + return err +} diff --git a/transaction/transaction.go b/transaction/transaction.go index b61f08be..20b64730 100644 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -14,6 +14,7 @@ const MinTxnFee = 1000 // MakePaymentTxn constructs a payment transaction using the passed parameters. // `from` and `to` addresses should be checksummed, human-readable addresses // fee is fee per byte as received from algod SuggestedFee API call +// Deprecated: next major version will use a Params object, see package future func MakePaymentTxn(from, to string, fee, amount, firstRound, lastRound uint64, note []byte, closeRemainderTo, genesisID string, genesisHash []byte) (types.Transaction, error) { // Decode from address fromAddr, err := types.DecodeAddress(from) @@ -64,7 +65,7 @@ func MakePaymentTxn(from, to string, fee, amount, firstRound, lastRound uint64, } // Update fee - eSize, err := estimateSize(tx) + eSize, err := EstimateSize(tx) if err != nil { return types.Transaction{}, err } @@ -80,6 +81,7 @@ func MakePaymentTxn(from, to string, fee, amount, firstRound, lastRound uint64, // MakePaymentTxnWithFlatFee constructs a payment transaction using the passed parameters. // `from` and `to` addresses should be checksummed, human-readable addresses // fee is a flat fee +// Deprecated: next major version will use a Params object, see package future func MakePaymentTxnWithFlatFee(from, to string, fee, amount, firstRound, lastRound uint64, note []byte, closeRemainderTo, genesisID string, genesisHash []byte) (types.Transaction, error) { tx, err := MakePaymentTxn(from, to, fee, amount, firstRound, lastRound, note, closeRemainderTo, genesisID, genesisHash) if err != nil { @@ -108,6 +110,7 @@ func MakePaymentTxnWithFlatFee(from, to string, fee, amount, firstRound, lastRou // - voteFirst is the first round this participation key is valid // - voteLast is the last round this participation key is valid // - voteKeyDilution is the dilution for the 2-level participation key +// Deprecated: next major version will use a Params object, see package future func MakeKeyRegTxn(account string, feePerByte, firstRound, lastRound uint64, note []byte, genesisID string, genesisHash string, voteKey, selectionKey string, voteFirst, voteLast, voteKeyDilution uint64) (types.Transaction, error) { // Decode account address @@ -152,7 +155,7 @@ func MakeKeyRegTxn(account string, feePerByte, firstRound, lastRound uint64, not } // Update fee - eSize, err := estimateSize(tx) + eSize, err := EstimateSize(tx) if err != nil { return types.Transaction{}, err } @@ -179,6 +182,7 @@ func MakeKeyRegTxn(account string, feePerByte, firstRound, lastRound uint64, not // - voteFirst is the first round this participation key is valid // - voteLast is the last round this participation key is valid // - voteKeyDilution is the dilution for the 2-level participation key +// Deprecated: next major version will use a Params object, see package future func MakeKeyRegTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, note []byte, genesisID string, genesisHash string, voteKey, selectionKey string, voteFirst, voteLast, voteKeyDilution uint64) (types.Transaction, error) { tx, err := MakeKeyRegTxn(account, fee, firstRound, lastRound, note, genesisID, genesisHash, voteKey, selectionKey, voteFirst, voteLast, voteKeyDilution) @@ -205,6 +209,7 @@ func MakeKeyRegTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // Asset creation parameters: // - see asset.go +// Deprecated: next major version will use a Params object, see package future func MakeAssetCreateTxn(account string, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, total uint64, decimals uint32, defaultFrozen bool, manager, reserve, freeze, clawback string, unitName, assetName, url, metadataHash string) (types.Transaction, error) { @@ -290,7 +295,7 @@ func MakeAssetCreateTxn(account string, feePerByte, firstRound, lastRound uint64 } // Update fee - eSize, err := estimateSize(tx) + eSize, err := EstimateSize(tx) if err != nil { return types.Transaction{}, err } @@ -320,6 +325,7 @@ func MakeAssetCreateTxn(account string, feePerByte, firstRound, lastRound uint64 // - index is the asset index id // - for newManager, newReserve, newFreeze, newClawback see asset.go // - strictEmptyAddressChecking: if true, disallow empty admin accounts from being set (preventing accidental disable of admin features) +// Deprecated: next major version will use a Params object, see package future func MakeAssetConfigTxn(account string, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64, newManager, newReserve, newFreeze, newClawback string, strictEmptyAddressChecking bool) (types.Transaction, error) { var tx types.Transaction @@ -382,7 +388,7 @@ func MakeAssetConfigTxn(account string, feePerByte, firstRound, lastRound uint64 } // Update fee - eSize, err := estimateSize(tx) + eSize, err := EstimateSize(tx) if err != nil { return types.Transaction{}, err } @@ -397,6 +403,7 @@ func MakeAssetConfigTxn(account string, feePerByte, firstRound, lastRound uint64 // transferAssetBuilder is a helper that builds asset transfer transactions: // either a normal asset transfer, or an asset revocation +// Deprecated: next major version will use a Params object, see package future func transferAssetBuilder(account, recipient, closeAssetsTo, revocationTarget string, amount, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64) (types.Transaction, error) { var tx types.Transaction @@ -449,7 +456,7 @@ func transferAssetBuilder(account, recipient, closeAssetsTo, revocationTarget st tx.AssetAmount = amount // Update fee - eSize, err := estimateSize(tx) + eSize, err := EstimateSize(tx) if err != nil { return types.Transaction{}, err } @@ -475,6 +482,7 @@ func transferAssetBuilder(account, recipient, closeAssetsTo, revocationTarget st // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetTransferTxn(account, recipient, closeAssetsTo string, amount, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64) (types.Transaction, error) { revocationTarget := "" // no asset revocation, this is normal asset transfer @@ -491,6 +499,7 @@ func MakeAssetTransferTxn(account, recipient, closeAssetsTo string, amount, feeP // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetAcceptanceTxn(account string, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64) (types.Transaction, error) { return MakeAssetTransferTxn(account, account, "", 0, @@ -508,6 +517,7 @@ func MakeAssetAcceptanceTxn(account string, feePerByte, firstRound, lastRound ui // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetRevocationTxn(account, target, recipient string, amount, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64) (types.Transaction, error) { closeAssetsTo := "" // no close-out, this is an asset revocation @@ -524,6 +534,7 @@ func MakeAssetRevocationTxn(account, target, recipient string, amount, feePerByt // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetDestroyTxn(account string, feePerByte, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64) (types.Transaction, error) { // an asset destroy transaction is just a configuration transaction with AssetParams zeroed @@ -545,6 +556,7 @@ func MakeAssetDestroyTxn(account string, feePerByte, firstRound, lastRound uint6 // - assetIndex is the index for tracking the asset // - target is the account to be frozen or unfrozen // - newFreezeSetting is the new state of the target account +// Deprecated: next major version will use a Params object, see package future func MakeAssetFreezeTxn(account string, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, assetIndex uint64, target string, newFreezeSetting bool) (types.Transaction, error) { var tx types.Transaction @@ -580,7 +592,7 @@ func MakeAssetFreezeTxn(account string, fee, firstRound, lastRound uint64, note tx.AssetFrozen = newFreezeSetting // Update fee - eSize, err := estimateSize(tx) + eSize, err := EstimateSize(tx) if err != nil { return types.Transaction{}, err } @@ -602,6 +614,7 @@ func MakeAssetFreezeTxn(account string, fee, firstRound, lastRound uint64, note // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // Asset creation parameters: // - see asset.go +// Deprecated: next major version will use a Params object, see package future func MakeAssetCreateTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, total uint64, decimals uint32, defaultFrozen bool, manager, reserve, freeze, clawback, unitName, assetName, url, metadataHash string) (types.Transaction, error) { tx, err := MakeAssetCreateTxn(account, fee, firstRound, lastRound, note, genesisID, genesisHash, total, decimals, defaultFrozen, manager, reserve, freeze, clawback, unitName, assetName, url, metadataHash) @@ -622,6 +635,7 @@ func MakeAssetCreateTxnWithFlatFee(account string, fee, firstRound, lastRound ui // keys for an asset. An empty string means a zero key (which // cannot be changed after becoming zero); to keep a key // unchanged, you must specify that key. +// Deprecated: next major version will use a Params object, see package future func MakeAssetConfigTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64, newManager, newReserve, newFreeze, newClawback string, strictEmptyAddressChecking bool) (types.Transaction, error) { tx, err := MakeAssetConfigTxn(account, fee, firstRound, lastRound, note, genesisID, genesisHash, @@ -674,6 +688,7 @@ func MakeAssetTransferTxnWithFlatFee(account, recipient, closeAssetsTo string, a // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetAcceptanceTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, index uint64) (types.Transaction, error) { tx, err := MakeAssetTransferTxnWithFlatFee(account, account, "", 0, @@ -692,6 +707,7 @@ func MakeAssetAcceptanceTxnWithFlatFee(account string, fee, firstRound, lastRoun // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetRevocationTxnWithFlatFee(account, target, recipient string, amount, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash, creator string, index uint64) (types.Transaction, error) { tx, err := MakeAssetRevocationTxn(account, target, recipient, amount, fee, firstRound, lastRound, @@ -718,6 +734,7 @@ func MakeAssetRevocationTxnWithFlatFee(account, target, recipient string, amount // - genesis id corresponds to the id of the network // - genesis hash corresponds to the base64-encoded hash of the genesis of the network // - index is the asset index +// Deprecated: next major version will use a Params object, see package future func MakeAssetDestroyTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, creator string, index uint64) (types.Transaction, error) { tx, err := MakeAssetConfigTxnWithFlatFee(account, fee, firstRound, lastRound, note, genesisID, genesisHash, @@ -726,6 +743,7 @@ func MakeAssetDestroyTxnWithFlatFee(account string, fee, firstRound, lastRound u } // MakeAssetFreezeTxnWithFlatFee is as MakeAssetFreezeTxn, but taking a flat fee rather than a fee per byte. +// Deprecated: next major version will use a Params object, see package future func MakeAssetFreezeTxnWithFlatFee(account string, fee, firstRound, lastRound uint64, note []byte, genesisID, genesisHash string, creator string, assetIndex uint64, target string, newFreezeSetting bool) (types.Transaction, error) { tx, err := MakeAssetFreezeTxn(account, fee, firstRound, lastRound, note, genesisID, genesisHash, @@ -767,7 +785,7 @@ func AssignGroupID(txns []types.Transaction, account string) (result []types.Tra } // EstimateSize returns the estimated length of the encoded transaction -func estimateSize(txn types.Transaction) (uint64, error) { +func EstimateSize(txn types.Transaction) (uint64, error) { key := crypto.GenerateAccount() _, stx, err := crypto.SignTransaction(key.PrivateKey, txn) if err != nil { diff --git a/types/address.go b/types/address.go index 4cbc28ff..6c81e28a 100644 --- a/types/address.go +++ b/types/address.go @@ -26,6 +26,13 @@ func (a Address) String() string { return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(checksumAddress) } +var ZeroAddress Address = [hashLenBytes]byte{} + +// IsZero returs true if the Address is all zero bytes. +func (a Address) IsZero() bool { + return a == ZeroAddress +} + // 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/asset.go b/types/asset.go index eaa3fbf1..4cda6d08 100644 --- a/types/asset.go +++ b/types/asset.go @@ -69,3 +69,11 @@ type AssetParams struct { // of this asset from any account. Clawback Address `codec:"c"` } + +var zeroAP = AssetParams{} + +// IsZero returns true if the AssetParams struct is completely empty. +// The AssetParams zero object is used in destroying an asset. +func (ap AssetParams) IsZero() bool { + return ap == zeroAP +} diff --git a/types/signature.go b/types/signature.go index f0b32976..738bb19a 100644 --- a/types/signature.go +++ b/types/signature.go @@ -56,3 +56,22 @@ type LogicSig struct { // Args are not signed, but checked by Logic Args [][]byte `codec:"arg"` } + +// Blank returns true iff the lsig is empty. We need this instead of just +// comparing with == LogicSig{}, because it contains slices. +func (lsig LogicSig) Blank() bool { + if lsig.Args != nil { + return false + } + if len(lsig.Logic) != 0 { + return false + } + if !lsig.Msig.Blank() { + return false + } + if lsig.Sig != (Signature{}) { + return false + } + return true +} + diff --git a/types/transaction.go b/types/transaction.go index 6e12ade5..d4d3dca2 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -148,6 +148,36 @@ type TxGroup struct { TxGroupHashes []Digest `codec:"txlist"` } +// SuggestedParams wraps the transaction parameters common to all transactions, +// typically received from the SuggestedParams endpoint of algod. +// This struct itself is not sent over the wire to or from algod: see models.TransactionParams. +type SuggestedParams struct { + // Fee is the suggested transaction fee + // Fee is in units of micro-Algos per byte. + // Fee may fall to zero but transactions must still have a fee of + // at least MinTxnFee for the current network protocol. + Fee MicroAlgos + + // Genesis ID + GenesisID string + + // Genesis hash + GenesisHash []byte + + // FirstRoundValid is the first protocol round on which the txn is valid + FirstRoundValid Round + + // LastRoundValid is the final protocol round on which the txn may be committed + LastRoundValid Round + + // ConsensusVersion indicates the consensus protocol version + // as of LastRound. + ConsensusVersion string + + // FlatFee indicates whether the passed fee is per-byte or per-transaction + FlatFee bool +} + // AddLease adds the passed lease (see types/transaction.go) to the header of the passed transaction // and updates fee accordingly // - lease: the [32]byte lease to add to the header