Skip to content

Commit

Permalink
Merge pull request #69 from coinbase/patrick/scenario-parsing
Browse files Browse the repository at this point in the history
Scenario Processing
  • Loading branch information
patrick-ogrady committed Jul 23, 2020
2 parents 3864299 + 7b8bd4e commit 33358ed
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 0 deletions.
116 changes: 116 additions & 0 deletions internal/scenario/scenario.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package scenario

import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"

"github.com/coinbase/rosetta-sdk-go/types"
)

const (
// Scenarios can contain one of many of the following reserved
// keywords that are automatically populated.

// Sender is the sender and signer of a transaction.
Sender = "{{ SENDER }}"

// SenderValue is the amount the sender is paying.
SenderValue = "{{ SENDER_VALUE }}"

// Recipient is the recipient of the transaction.
Recipient = "{{ RECIPIENT }}"

// RecipientValue is the amount the recipient is
// receiving from the sender. Note, this is distinct
// from the SenderValue so that UTXO transfers
// can be supported.
RecipientValue = "{{ RECIPIENT_VALUE }}"

// UTXOIdentifier is the globally unique identifier
// of a UTXO. This should be in the Operation.metadata
// of any UTXO-based blockchain ("utxo_created" when
// a new UTXO is created and "utxo_spent" when a
// UTXO is spent).
UTXOIdentifier = "{{ UTXO_IDENTIFIER }}"
)

// Context is all information passed to PopulateScenario.
// As more exotic scenario testing is supported, this will
// likely be expanded.
type Context struct {
Sender string
SenderValue *big.Int
Recipient string
RecipientValue *big.Int
UTXOIdentifier string
Currency *types.Currency
}

// PopulateScenario populates a provided scenario (slice of
// []*types.Operation) with the information in Context.
func PopulateScenario(
ctx context.Context,
scenarioContext *Context,
scenario []*types.Operation,
) ([]*types.Operation, error) {
// Convert operations to a string
bytes, err := json.Marshal(scenario)
if err != nil {
return nil, fmt.Errorf("%w: unable to marshal scenario", err)
}

// Replace all keywords with information in Context
stringBytes := string(bytes)
stringBytes = strings.ReplaceAll(stringBytes, Sender, scenarioContext.Sender)
stringBytes = strings.ReplaceAll(
stringBytes,
SenderValue,
new(big.Int).Neg(scenarioContext.SenderValue).String(),
)
stringBytes = strings.ReplaceAll(stringBytes, Recipient, scenarioContext.Recipient)
stringBytes = strings.ReplaceAll(
stringBytes,
RecipientValue,
new(big.Int).Abs(scenarioContext.RecipientValue).String(),
)

if len(scenarioContext.UTXOIdentifier) > 0 {
stringBytes = strings.ReplaceAll(
stringBytes,
UTXOIdentifier,
scenarioContext.UTXOIdentifier,
)
}

// Convert back to ops
var ops []*types.Operation
if err := json.Unmarshal([]byte(stringBytes), &ops); err != nil {
return nil, fmt.Errorf("%w: unable to unmarshal ops", err)
}

// Post-process operations
for _, op := range ops {
if op.Amount != nil {
op.Amount.Currency = scenarioContext.Currency
}
}

return ops, nil
}
208 changes: 208 additions & 0 deletions internal/scenario/scenario_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package scenario

import (
"context"
"math/big"
"testing"

"github.com/coinbase/rosetta-sdk-go/types"
"github.com/stretchr/testify/assert"
)

var (
sender = "addr1"
senderValue = big.NewInt(100)
recipient = "addr2"
recipientValue = big.NewInt(90)
utxoIdentifier = "utxo1"

bitcoinCurrency = &types.Currency{
Symbol: "BTC",
Decimals: 8,
}
ethereumCurrency = &types.Currency{
Symbol: "ETH",
Decimals: 18,
}
)

func TestPopulateScenario(t *testing.T) {
var tests = map[string]struct {
context *Context
scenario []*types.Operation

expected []*types.Operation
}{
"bitcoin": {
context: &Context{
Sender: sender,
SenderValue: senderValue,
Recipient: recipient,
RecipientValue: recipientValue,
UTXOIdentifier: utxoIdentifier,
Currency: bitcoinCurrency,
},
scenario: []*types.Operation{
{
Type: "Vin",
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Account: &types.AccountIdentifier{
Address: "{{ SENDER }}",
},
Amount: &types.Amount{
Value: "{{ SENDER_VALUE }}",
},
Metadata: map[string]interface{}{
"utxo_spent": "{{ UTXO_IDENTIFIER }}",
},
},
{
Type: "Vout",
OperationIdentifier: &types.OperationIdentifier{
Index: 1,
},
Account: &types.AccountIdentifier{
Address: "{{ RECIPIENT }}",
},
Amount: &types.Amount{
Value: "{{ RECIPIENT_VALUE }}",
},
},
},
expected: []*types.Operation{
{
Type: "Vin",
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Account: &types.AccountIdentifier{
Address: sender,
},
Amount: &types.Amount{
Value: new(big.Int).Neg(senderValue).String(),
Currency: bitcoinCurrency,
},
Metadata: map[string]interface{}{
"utxo_spent": utxoIdentifier,
},
},
{
Type: "Vout",
OperationIdentifier: &types.OperationIdentifier{
Index: 1,
},
Account: &types.AccountIdentifier{
Address: recipient,
},
Amount: &types.Amount{
Value: new(big.Int).Abs(recipientValue).String(),
Currency: bitcoinCurrency,
},
},
},
},
"ethereum": {
context: &Context{
Sender: sender,
SenderValue: senderValue,
Recipient: recipient,
RecipientValue: recipientValue,
Currency: ethereumCurrency,
},
scenario: []*types.Operation{
{
Type: "transfer",
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Account: &types.AccountIdentifier{
Address: "{{ SENDER }}",
},
Amount: &types.Amount{
Value: "{{ SENDER_VALUE }}",
},
},
{
Type: "transfer",
OperationIdentifier: &types.OperationIdentifier{
Index: 1,
},
RelatedOperations: []*types.OperationIdentifier{
{
Index: 0,
},
},
Account: &types.AccountIdentifier{
Address: "{{ RECIPIENT }}",
},
Amount: &types.Amount{
Value: "{{ RECIPIENT_VALUE }}",
},
},
},
expected: []*types.Operation{
{
Type: "transfer",
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Account: &types.AccountIdentifier{
Address: sender,
},
Amount: &types.Amount{
Value: new(big.Int).Neg(senderValue).String(),
Currency: ethereumCurrency,
},
},
{
Type: "transfer",
OperationIdentifier: &types.OperationIdentifier{
Index: 1,
},
RelatedOperations: []*types.OperationIdentifier{
{
Index: 0,
},
},
Account: &types.AccountIdentifier{
Address: recipient,
},
Amount: &types.Amount{
Value: new(big.Int).Abs(recipientValue).String(),
Currency: ethereumCurrency,
},
},
},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
ctx := context.Background()

ops, err := PopulateScenario(
ctx,
test.context,
test.scenario,
)
assert.NoError(t, err)
assert.ElementsMatch(t, test.expected, ops)
})
}
}

0 comments on commit 33358ed

Please sign in to comment.