diff --git a/CHANGELOG.md b/CHANGELOG.md index fa1dc93..7d3554a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Added - GitHub Actions CI with test, lint (golangci-lint), and vulncheck jobs +- `TransferBody.TransactionValue` (TAIP-3) — optional fiat-equivalent value + (`amount`, `currency`) for Travel Rule threshold determination when an asset + is not widely traded ### Changed -- Bumped Go from 1.25.0 to 1.25.3 to fix stdlib vulnerabilities +- Bumped Go from 1.25.0 to 1.26.2 to fix stdlib vulnerabilities - Use published go-didcomm v0.1.0 instead of local replace directive +- **BREAKING (TAIP-17):** Renamed `Escrow` message type to `Lock`. Constants, + types, constructors, files, and CLI subcommand all renamed: + `TypeEscrow` → `TypeLock`, `EscrowBody` → `LockBody`, + `NewEscrowMessage` → `NewLockMessage`, `escrow.go` → `lock.go`, + `tap message escrow` → `tap message lock`. The `EscrowAgent` role name is + preserved. +- **BREAKING (TAIP-18):** Renamed `Exchange` message type to `RFQ` (Request + for Quote). `TypeExchange` → `TypeRFQ`, `ExchangeBody` → `RFQBody`, + `NewExchangeMessage` → `NewRFQMessage`, `exchange.go` → `rfq.go`, + `tap message exchange` → `tap message rfq`. diff --git a/CLAUDE.md b/CLAUDE.md index d8c6750..cf9f64d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,9 +53,9 @@ Single flat `tap` package — all types and helpers in the root. One file per me | `client.go` | `Client` wrapping `didcomm.Client`, `Receive()` returning `TAPResult` | | `transfer.go` | `TransferBody` + `NewTransferMessage()` | | `payment.go` | `PaymentBody` + `NewPaymentMessage()` | -| `exchange.go` | `ExchangeBody` + `NewExchangeMessage()` | +| `rfq.go` | `RFQBody` + `NewRFQMessage()` | | `quote.go` | `QuoteBody` + `NewQuoteMessage()` | -| `escrow.go` | `EscrowBody` + `NewEscrowMessage()` | +| `lock.go` | `LockBody` + `NewLockMessage()` | | `authorize.go` | `AuthorizeBody` + `NewAuthorizeMessage()` | | `authorization_required.go` | `AuthorizationRequiredBody` + `NewAuthorizationRequiredMessage()` | | `settle.go` | `SettleBody` + `NewSettleMessage()` | diff --git a/README.md b/README.md index 51943db..0bfc186 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,9 @@ envelope, _ := dc.PackAuthcrypt(ctx, authorizeMsg) |------------|-------------|------|-----------------| | `NewTransferMessage` | `TransferBody` | [TAIP-3](https://tap.rsvp/TAIPs/taip-3) | `asset`, `agents` | | `NewPaymentMessage` | `PaymentBody` | [TAIP-14](https://tap.rsvp/TAIPs/taip-14) | `amount`, `merchant`, `agents`, `asset` or `currency` | -| `NewExchangeMessage` | `ExchangeBody` | [TAIP-18](https://tap.rsvp/TAIPs/taip-18) | `fromAssets`, `toAssets`, `requester`, `agents`, `fromAmount` or `toAmount` | +| `NewRFQMessage` | `RFQBody` | [TAIP-18](https://tap.rsvp/TAIPs/taip-18) | `fromAssets`, `toAssets`, `requester`, `agents`, `fromAmount` or `toAmount` | | `NewQuoteMessage` | `QuoteBody` | [TAIP-18](https://tap.rsvp/TAIPs/taip-18) | `fromAsset`, `toAsset`, `fromAmount`, `toAmount`, `provider`, `agents`, `expiresAt` | -| `NewEscrowMessage` | `EscrowBody` | [TAIP-17](https://tap.rsvp/TAIPs/taip-17) | `amount`, `originator`, `beneficiary`, `expiry`, `agents`, `asset` or `currency` | +| `NewLockMessage` | `LockBody` | [TAIP-17](https://tap.rsvp/TAIPs/taip-17) | `amount`, `originator`, `beneficiary`, `expiry`, `agents`, `asset` or `currency` | ### Authorization Flow Messages @@ -331,7 +331,7 @@ The `receive` command outputs JSON with the unpacked message, typed body, and en ### TAP message types -**Initiating (no `--thid`):** `transfer`, `payment`, `exchange`, `escrow`, `connect` +**Initiating (no `--thid`):** `transfer`, `payment`, `rfq`, `lock`, `connect` **Reply (require `--thid`):** `authorize`, `authorization-required`, `settle`, `reject`, `cancel`, `revert`, `capture`, `quote`, `add-agents`, `remove-agent`, `replace-agent`, `update-agent`, `update-party`, `update-policies`, `confirm-relationship` diff --git a/cmd/tap/main.go b/cmd/tap/main.go index e843b33..ef88da8 100644 --- a/cmd/tap/main.go +++ b/cmd/tap/main.go @@ -28,7 +28,7 @@ Commands: help Print this help TAP message types: - Initiating: transfer, payment, exchange, escrow, connect + Initiating: transfer, payment, rfq, lock, connect Reply: authorize, authorization-required, settle, reject, cancel, revert, capture, quote, add-agents, remove-agent, replace-agent, update-agent, update-party, update-policies, diff --git a/cmd/tap/message.go b/cmd/tap/message.go index 4462e5e..2f2772f 100644 --- a/cmd/tap/message.go +++ b/cmd/tap/message.go @@ -16,7 +16,7 @@ import ( const messageUsage = `Usage: tap message --from --to [--thid ] [--body ] Initiating types (no --thid): - transfer, payment, exchange, escrow, connect + transfer, payment, rfq, lock, connect Reply types (require --thid): authorize, authorization-required, settle, reject, cancel, revert, @@ -111,10 +111,10 @@ func runMessage(args []string) error { return runMessageTransfer(msgArgs) case "payment": return runMessagePayment(msgArgs) - case "exchange": - return runMessageExchange(msgArgs) - case "escrow": - return runMessageEscrow(msgArgs) + case "rfq": + return runMessageRFQ(msgArgs) + case "lock": + return runMessageLock(msgArgs) case "connect": return runMessageConnect(msgArgs) @@ -189,32 +189,32 @@ func runMessagePayment(args []string) error { return writeMessage(msg) } -func runMessageExchange(args []string) error { - f, err := parseMessageFlags("exchange", args, false) +func runMessageRFQ(args []string) error { + f, err := parseMessageFlags("rfq", args, false) if err != nil { return err } - var body tap.ExchangeBody + var body tap.RFQBody if err := json.Unmarshal(f.body, &body); err != nil { return fmt.Errorf("parse body JSON: %w", err) } - msg, err := tap.NewExchangeMessage(f.from, f.to, &body) + msg, err := tap.NewRFQMessage(f.from, f.to, &body) if err != nil { return err } return writeMessage(msg) } -func runMessageEscrow(args []string) error { - f, err := parseMessageFlags("escrow", args, false) +func runMessageLock(args []string) error { + f, err := parseMessageFlags("lock", args, false) if err != nil { return err } - var body tap.EscrowBody + var body tap.LockBody if err := json.Unmarshal(f.body, &body); err != nil { return fmt.Errorf("parse body JSON: %w", err) } - msg, err := tap.NewEscrowMessage(f.from, f.to, &body) + msg, err := tap.NewLockMessage(f.from, f.to, &body) if err != nil { return err } diff --git a/cmd/tap/message_test.go b/cmd/tap/message_test.go index 57920d6..7534f5e 100644 --- a/cmd/tap/message_test.go +++ b/cmd/tap/message_test.go @@ -237,49 +237,49 @@ func TestCLI_MessagePayment(t *testing.T) { } } -func TestCLI_MessageExchange(t *testing.T) { +func TestCLI_MessageRFQ(t *testing.T) { bin := buildBinary(t) body := `{"fromAssets":["eip155:1/slip44:60"],"toAssets":["eip155:1/slip44:0"],"fromAmount":"1.0","requester":{"@id":"did:key:z1"},"agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` - cmd := exec.Command(bin, "message", "exchange", + cmd := exec.Command(bin, "message", "rfq", "--from", "did:key:z1", "--to", "did:key:z2", "--body", body, ) out, err := cmd.Output() if err != nil { - t.Fatalf("message exchange failed: %s", err) + t.Fatalf("message rfq failed: %s", err) } var msg didcomm.Message if err := json.Unmarshal(out, &msg); err != nil { t.Fatalf("invalid JSON: %s", err) } - if msg.Type != tap.TypeExchange { - t.Fatalf("expected type %s, got %s", tap.TypeExchange, msg.Type) + if msg.Type != tap.TypeRFQ { + t.Fatalf("expected type %s, got %s", tap.TypeRFQ, msg.Type) } } -func TestCLI_MessageEscrow(t *testing.T) { +func TestCLI_MessageLock(t *testing.T) { bin := buildBinary(t) body := `{"asset":"eip155:1/slip44:60","amount":"5.0","originator":{"@id":"did:key:z1"},"beneficiary":{"@id":"did:key:z2"},"expiry":"2025-12-31T23:59:59Z","agents":[{"@id":"did:key:z1","role":"OriginatingVASP"}]}` - cmd := exec.Command(bin, "message", "escrow", + cmd := exec.Command(bin, "message", "lock", "--from", "did:key:z1", "--to", "did:key:z2", "--body", body, ) out, err := cmd.Output() if err != nil { - t.Fatalf("message escrow failed: %s", err) + t.Fatalf("message lock failed: %s", err) } var msg didcomm.Message if err := json.Unmarshal(out, &msg); err != nil { t.Fatalf("invalid JSON: %s", err) } - if msg.Type != tap.TypeEscrow { - t.Fatalf("expected type %s, got %s", tap.TypeEscrow, msg.Type) + if msg.Type != tap.TypeLock { + t.Fatalf("expected type %s, got %s", tap.TypeLock, msg.Type) } } diff --git a/doc.go b/doc.go index 13fc4e1..38ce3dd 100644 --- a/doc.go +++ b/doc.go @@ -16,9 +16,9 @@ // Transaction messages initiate financial operations: // - Transfer (TAIP-3) — asset transfer between parties // - Payment (TAIP-14) — merchant payment request -// - Exchange (TAIP-18) — asset exchange request +// - RFQ (TAIP-18) — request for quote on an asset exchange // - Quote (TAIP-18) — exchange price quote response -// - Escrow (TAIP-17) — hold funds in escrow +// - Lock (TAIP-17) — hold funds in escrow // // Authorization flow messages manage transaction lifecycle: // - Authorize (TAIP-4) — approve a transaction @@ -27,7 +27,7 @@ // - Reject (TAIP-4) — reject a transaction // - Cancel (TAIP-4) — cancel a transaction // - Revert (TAIP-4) — request reversal of settled transaction -// - Capture (TAIP-17) — release escrowed funds +// - Capture (TAIP-17) — release locked funds // // Agent management messages modify transaction participants: // - UpdateAgent (TAIP-5) — update agent information diff --git a/go.mod b/go.mod index 3f78d6b..63fcb81 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/TransactionAuthorizationProtocol/tap-go -go 1.26.0 +go 1.26.2 require ( github.com/Notabene-id/go-didcomm v0.2.0 diff --git a/escrow.go b/lock.go similarity index 79% rename from escrow.go rename to lock.go index 18d6fa3..a712881 100644 --- a/escrow.go +++ b/lock.go @@ -8,8 +8,8 @@ import ( "github.com/google/uuid" ) -// EscrowBody represents the body of a TAP Escrow message (TAIP-17). -type EscrowBody struct { +// LockBody represents the body of a TAP Lock message (TAIP-17). +type LockBody struct { Context string `json:"@context"` Type string `json:"@type"` Asset string `json:"asset,omitempty"` @@ -22,10 +22,10 @@ type EscrowBody struct { Agreement string `json:"agreement,omitempty"` } -func (b *EscrowBody) TAPType() string { return TypeEscrow } +func (b *LockBody) TAPType() string { return TypeLock } -// NewEscrowMessage creates a new DIDComm message with an Escrow body. -func NewEscrowMessage(from string, to []string, body *EscrowBody) (*didcomm.Message, error) { +// NewLockMessage creates a new DIDComm message with a Lock body. +func NewLockMessage(from string, to []string, body *LockBody) (*didcomm.Message, error) { if body.Amount == "" { return nil, fmt.Errorf("%w: missing amount", ErrInvalidBody) } @@ -46,7 +46,7 @@ func NewEscrowMessage(from string, to []string, body *EscrowBody) (*didcomm.Mess } body.Context = TAPContext - body.Type = TypeEscrow + body.Type = TypeLock rawBody, err := json.Marshal(body) if err != nil { @@ -55,7 +55,7 @@ func NewEscrowMessage(from string, to []string, body *EscrowBody) (*didcomm.Mess return &didcomm.Message{ ID: uuid.New().String(), - Type: TypeEscrow, + Type: TypeLock, From: from, To: to, Body: rawBody, diff --git a/escrow_test.go b/lock_test.go similarity index 67% rename from escrow_test.go rename to lock_test.go index 52a9594..0a5b1c0 100644 --- a/escrow_test.go +++ b/lock_test.go @@ -7,8 +7,8 @@ import ( "testing" ) -func TestNewEscrowMessage(t *testing.T) { - body := &EscrowBody{ +func TestNewLockMessage(t *testing.T) { + body := &LockBody{ Asset: "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", Amount: "1000.00", Originator: &Party{ID: "did:web:buyer.example"}, @@ -19,17 +19,17 @@ func TestNewEscrowMessage(t *testing.T) { }, } - msg, err := NewEscrowMessage("did:web:buyer.example", []string{"did:web:escrow-service.example"}, body) + msg, err := NewLockMessage("did:web:buyer.example", []string{"did:web:escrow-service.example"}, body) if err != nil { t.Fatalf("unexpected error: %v", err) } - if msg.Type != TypeEscrow { + if msg.Type != TypeLock { t.Errorf("Type: got %q", msg.Type) } } -func TestNewEscrowMessage_MissingFields(t *testing.T) { - base := EscrowBody{ +func TestNewLockMessage_MissingFields(t *testing.T) { + base := LockBody{ Asset: "eip155:1/erc20:0xA0b86991", Amount: "1000.00", Originator: &Party{ID: "did:web:buyer"}, @@ -40,21 +40,21 @@ func TestNewEscrowMessage_MissingFields(t *testing.T) { tests := []struct { name string - modify func(*EscrowBody) + modify func(*LockBody) }{ - {"missing amount", func(b *EscrowBody) { b.Amount = "" }}, - {"missing originator", func(b *EscrowBody) { b.Originator = nil }}, - {"missing beneficiary", func(b *EscrowBody) { b.Beneficiary = nil }}, - {"missing expiry", func(b *EscrowBody) { b.Expiry = "" }}, - {"missing agents", func(b *EscrowBody) { b.Agents = nil }}, - {"missing asset and currency", func(b *EscrowBody) { b.Asset = "" }}, + {"missing amount", func(b *LockBody) { b.Amount = "" }}, + {"missing originator", func(b *LockBody) { b.Originator = nil }}, + {"missing beneficiary", func(b *LockBody) { b.Beneficiary = nil }}, + {"missing expiry", func(b *LockBody) { b.Expiry = "" }}, + {"missing agents", func(b *LockBody) { b.Agents = nil }}, + {"missing asset and currency", func(b *LockBody) { b.Asset = "" }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := base tt.modify(&b) - _, err := NewEscrowMessage("from", nil, &b) + _, err := NewLockMessage("from", nil, &b) if !errors.Is(err, ErrInvalidBody) { t.Errorf("expected ErrInvalidBody, got %v", err) } @@ -62,10 +62,10 @@ func TestNewEscrowMessage_MissingFields(t *testing.T) { } } -func TestEscrowBody_JSONRoundTrip(t *testing.T) { - body := EscrowBody{ +func TestLockBody_JSONRoundTrip(t *testing.T) { + body := LockBody{ Context: TAPContext, - Type: TypeEscrow, + Type: TypeLock, Asset: "eip155:1/erc20:0xa0b86991", Amount: "1000.00", Originator: &Party{ID: "did:web:buyer.example"}, @@ -79,7 +79,7 @@ func TestEscrowBody_JSONRoundTrip(t *testing.T) { t.Fatalf("marshal: %v", err) } - var got EscrowBody + var got LockBody if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal: %v", err) } @@ -88,8 +88,8 @@ func TestEscrowBody_JSONRoundTrip(t *testing.T) { } } -func TestEscrow_TestVectorValid(t *testing.T) { - data, err := os.ReadFile("TAIPs/test-vectors/escrow/valid-escrow.json") +func TestLock_TestVectorValid(t *testing.T) { + data, err := os.ReadFile("TAIPs/test-vectors/lock/valid-lock.json") if err != nil { t.Skipf("test vector not available: %v", err) } @@ -103,7 +103,7 @@ func TestEscrow_TestVectorValid(t *testing.T) { t.Fatalf("unmarshal test vector: %v", err) } - var body EscrowBody + var body LockBody if err := json.Unmarshal(tv.Message.Body, &body); err != nil { t.Fatalf("unmarshal body: %v", err) } @@ -119,8 +119,8 @@ func TestEscrow_TestVectorValid(t *testing.T) { } } -func TestEscrowBody_ParseBody(t *testing.T) { - body := &EscrowBody{ +func TestLockBody_ParseBody(t *testing.T) { + body := &LockBody{ Asset: "eip155:1/erc20:0xa0b86991", Amount: "500.00", Originator: &Party{ID: "did:web:buyer"}, @@ -128,7 +128,7 @@ func TestEscrowBody_ParseBody(t *testing.T) { Expiry: "2024-12-31T00:00:00Z", Agents: []Agent{{ID: "did:web:escrow", Role: "EscrowAgent"}}, } - msg, err := NewEscrowMessage("did:web:buyer", []string{"did:web:escrow"}, body) + msg, err := NewLockMessage("did:web:buyer", []string{"did:web:escrow"}, body) if err != nil { t.Fatalf("create: %v", err) } @@ -137,11 +137,11 @@ func TestEscrowBody_ParseBody(t *testing.T) { if err != nil { t.Fatalf("parse: %v", err) } - eb, ok := parsed.(*EscrowBody) + lb, ok := parsed.(*LockBody) if !ok { - t.Fatalf("expected *EscrowBody, got %T", parsed) + t.Fatalf("expected *LockBody, got %T", parsed) } - if eb.Amount != "500.00" { - t.Errorf("Amount: got %q", eb.Amount) + if lb.Amount != "500.00" { + t.Errorf("Amount: got %q", lb.Amount) } } diff --git a/message_types.go b/message_types.go index be3f525..c0211bf 100644 --- a/message_types.go +++ b/message_types.go @@ -7,9 +7,9 @@ const ( // Transaction message types TypeTransfer = "https://tap.rsvp/schema/1.0#Transfer" TypePayment = "https://tap.rsvp/schema/1.0#Payment" - TypeExchange = "https://tap.rsvp/schema/1.0#Exchange" + TypeRFQ = "https://tap.rsvp/schema/1.0#RFQ" TypeQuote = "https://tap.rsvp/schema/1.0#Quote" - TypeEscrow = "https://tap.rsvp/schema/1.0#Escrow" + TypeLock = "https://tap.rsvp/schema/1.0#Lock" // Authorization flow message types TypeAuthorize = "https://tap.rsvp/schema/1.0#Authorize" @@ -38,9 +38,9 @@ func AllTypes() []string { return []string{ TypeTransfer, TypePayment, - TypeExchange, + TypeRFQ, TypeQuote, - TypeEscrow, + TypeLock, TypeAuthorize, TypeAuthorizationRequired, TypeSettle, diff --git a/message_types_test.go b/message_types_test.go index 9c57cff..bb4c9a5 100644 --- a/message_types_test.go +++ b/message_types_test.go @@ -9,9 +9,9 @@ func TestTypeConstants(t *testing.T) { types := map[string]string{ "Transfer": TypeTransfer, "Payment": TypePayment, - "Exchange": TypeExchange, + "RFQ": TypeRFQ, "Quote": TypeQuote, - "Escrow": TypeEscrow, + "Lock": TypeLock, "Authorize": TypeAuthorize, "AuthorizationRequired": TypeAuthorizationRequired, "Settle": TypeSettle, diff --git a/parse.go b/parse.go index bdc1862..9d9d129 100644 --- a/parse.go +++ b/parse.go @@ -32,12 +32,12 @@ func ParseBody(msg *didcomm.Message) (TAPBody, error) { body = &TransferBody{} case TypePayment: body = &PaymentBody{} - case TypeExchange: - body = &ExchangeBody{} + case TypeRFQ: + body = &RFQBody{} case TypeQuote: body = &QuoteBody{} - case TypeEscrow: - body = &EscrowBody{} + case TypeLock: + body = &LockBody{} case TypeAuthorize: body = &AuthorizeBody{} case TypeAuthorizationRequired: diff --git a/parse_test.go b/parse_test.go index fb2d255..358ff85 100644 --- a/parse_test.go +++ b/parse_test.go @@ -15,9 +15,9 @@ func TestIsTAPMessage(t *testing.T) { }{ {TypeTransfer, true}, {TypePayment, true}, - {TypeExchange, true}, + {TypeRFQ, true}, {TypeQuote, true}, - {TypeEscrow, true}, + {TypeLock, true}, {TypeAuthorize, true}, {TypeAuthorizationRequired, true}, {TypeSettle, true}, @@ -53,9 +53,9 @@ func TestParseBody_AllTypes(t *testing.T) { }{ {"Transfer", TypeTransfer, &TransferBody{Context: TAPContext, Type: TypeTransfer, Asset: "ETH", Agents: []Agent{{ID: "a"}}}}, {"Payment", TypePayment, &PaymentBody{Context: TAPContext, Type: TypePayment, Amount: "100", Currency: "USD", Merchant: &Party{ID: "m"}, Agents: []Agent{{ID: "a"}}}}, - {"Exchange", TypeExchange, &ExchangeBody{Context: TAPContext, Type: TypeExchange, FromAssets: []string{"ETH"}, ToAssets: []string{"USD"}, FromAmount: "1", Requester: &Party{ID: "r"}, Agents: []Agent{{ID: "a"}}}}, + {"RFQ", TypeRFQ, &RFQBody{Context: TAPContext, Type: TypeRFQ, FromAssets: []string{"ETH"}, ToAssets: []string{"USD"}, FromAmount: "1", Requester: &Party{ID: "r"}, Agents: []Agent{{ID: "a"}}}}, {"Quote", TypeQuote, &QuoteBody{Context: TAPContext, Type: TypeQuote, FromAsset: "ETH", ToAsset: "USD", FromAmount: "1", ToAmount: "3000", Provider: &Party{ID: "p"}, Agents: []Agent{{ID: "a"}}, ExpiresAt: "2025-01-01T00:00:00Z"}}, - {"Escrow", TypeEscrow, &EscrowBody{Context: TAPContext, Type: TypeEscrow, Asset: "ETH", Amount: "100", Originator: &Party{ID: "o"}, Beneficiary: &Party{ID: "b"}, Expiry: "2025-01-01T00:00:00Z", Agents: []Agent{{ID: "a"}}}}, + {"Lock", TypeLock, &LockBody{Context: TAPContext, Type: TypeLock, Asset: "ETH", Amount: "100", Originator: &Party{ID: "o"}, Beneficiary: &Party{ID: "b"}, Expiry: "2025-01-01T00:00:00Z", Agents: []Agent{{ID: "a"}}}}, {"Authorize", TypeAuthorize, &AuthorizeBody{Context: TAPContext, Type: TypeAuthorize}}, {"AuthorizationRequired", TypeAuthorizationRequired, &AuthorizationRequiredBody{Context: TAPContext, Type: TypeAuthorizationRequired, AuthorizationURL: "https://example.com", Expires: "2025-01-01T00:00:00Z"}}, {"Settle", TypeSettle, &SettleBody{Context: TAPContext, Type: TypeSettle, SettlementAddress: "eip155:1:0x1234"}}, diff --git a/exchange.go b/rfq.go similarity index 77% rename from exchange.go rename to rfq.go index 4ce75ed..ec3a0c7 100644 --- a/exchange.go +++ b/rfq.go @@ -8,8 +8,8 @@ import ( "github.com/google/uuid" ) -// ExchangeBody represents the body of a TAP Exchange message (TAIP-18). -type ExchangeBody struct { +// RFQBody represents the body of a TAP RFQ (Request for Quote) message (TAIP-18). +type RFQBody struct { Context string `json:"@context"` Type string `json:"@type"` FromAssets []string `json:"fromAssets"` @@ -22,10 +22,10 @@ type ExchangeBody struct { Policies []Policy `json:"policies,omitempty"` } -func (b *ExchangeBody) TAPType() string { return TypeExchange } +func (b *RFQBody) TAPType() string { return TypeRFQ } -// NewExchangeMessage creates a new DIDComm message with an Exchange body. -func NewExchangeMessage(from string, to []string, body *ExchangeBody) (*didcomm.Message, error) { +// NewRFQMessage creates a new DIDComm message with an RFQ body. +func NewRFQMessage(from string, to []string, body *RFQBody) (*didcomm.Message, error) { if len(body.FromAssets) == 0 { return nil, fmt.Errorf("%w: missing fromAssets", ErrInvalidBody) } @@ -43,7 +43,7 @@ func NewExchangeMessage(from string, to []string, body *ExchangeBody) (*didcomm. } body.Context = TAPContext - body.Type = TypeExchange + body.Type = TypeRFQ rawBody, err := json.Marshal(body) if err != nil { @@ -52,7 +52,7 @@ func NewExchangeMessage(from string, to []string, body *ExchangeBody) (*didcomm. return &didcomm.Message{ ID: uuid.New().String(), - Type: TypeExchange, + Type: TypeRFQ, From: from, To: to, Body: rawBody, diff --git a/exchange_test.go b/rfq_test.go similarity index 67% rename from exchange_test.go rename to rfq_test.go index ee7c1ed..0aeb9af 100644 --- a/exchange_test.go +++ b/rfq_test.go @@ -6,8 +6,8 @@ import ( "testing" ) -func TestNewExchangeMessage(t *testing.T) { - body := &ExchangeBody{ +func TestNewRFQMessage(t *testing.T) { + body := &RFQBody{ FromAssets: []string{"eip155:1/slip44:60"}, ToAssets: []string{"USD"}, FromAmount: "1.0", @@ -15,15 +15,15 @@ func TestNewExchangeMessage(t *testing.T) { Agents: []Agent{{ID: "did:web:exchange.example", For: NewForField("did:eg:alice")}}, } - msg, err := NewExchangeMessage("did:web:exchange.example", []string{"did:web:provider.example"}, body) + msg, err := NewRFQMessage("did:web:exchange.example", []string{"did:web:provider.example"}, body) if err != nil { t.Fatalf("unexpected error: %v", err) } - if msg.Type != TypeExchange { + if msg.Type != TypeRFQ { t.Errorf("Type: got %q", msg.Type) } - var got ExchangeBody + var got RFQBody if err := json.Unmarshal(msg.Body, &got); err != nil { t.Fatalf("unmarshal: %v", err) } @@ -32,36 +32,36 @@ func TestNewExchangeMessage(t *testing.T) { } } -func TestNewExchangeMessage_MissingFromAssets(t *testing.T) { - body := &ExchangeBody{ +func TestNewRFQMessage_MissingFromAssets(t *testing.T) { + body := &RFQBody{ ToAssets: []string{"USD"}, FromAmount: "1.0", Requester: &Party{ID: "did:eg:alice"}, Agents: []Agent{{ID: "did:web:exchange.example"}}, } - _, err := NewExchangeMessage("from", nil, body) + _, err := NewRFQMessage("from", nil, body) if !errors.Is(err, ErrInvalidBody) { t.Errorf("expected ErrInvalidBody, got %v", err) } } -func TestNewExchangeMessage_MissingAmounts(t *testing.T) { - body := &ExchangeBody{ +func TestNewRFQMessage_MissingAmounts(t *testing.T) { + body := &RFQBody{ FromAssets: []string{"ETH"}, ToAssets: []string{"USD"}, Requester: &Party{ID: "did:eg:alice"}, Agents: []Agent{{ID: "did:web:exchange.example"}}, } - _, err := NewExchangeMessage("from", nil, body) + _, err := NewRFQMessage("from", nil, body) if !errors.Is(err, ErrInvalidBody) { t.Errorf("expected ErrInvalidBody, got %v", err) } } -func TestExchangeBody_JSONRoundTrip(t *testing.T) { - body := ExchangeBody{ +func TestRFQBody_JSONRoundTrip(t *testing.T) { + body := RFQBody{ Context: TAPContext, - Type: TypeExchange, + Type: TypeRFQ, FromAssets: []string{"eip155:1/slip44:60"}, ToAssets: []string{"USD"}, FromAmount: "1.0", @@ -74,7 +74,7 @@ func TestExchangeBody_JSONRoundTrip(t *testing.T) { t.Fatalf("marshal: %v", err) } - var got ExchangeBody + var got RFQBody if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal: %v", err) } @@ -83,15 +83,15 @@ func TestExchangeBody_JSONRoundTrip(t *testing.T) { } } -func TestExchangeBody_ParseBody(t *testing.T) { - body := &ExchangeBody{ +func TestRFQBody_ParseBody(t *testing.T) { + body := &RFQBody{ FromAssets: []string{"ETH"}, ToAssets: []string{"USD"}, ToAmount: "3000.00", Requester: &Party{ID: "did:eg:alice"}, Agents: []Agent{{ID: "did:web:exchange.example"}}, } - msg, err := NewExchangeMessage("did:web:exchange.example", nil, body) + msg, err := NewRFQMessage("did:web:exchange.example", nil, body) if err != nil { t.Fatalf("create: %v", err) } @@ -100,11 +100,11 @@ func TestExchangeBody_ParseBody(t *testing.T) { if err != nil { t.Fatalf("parse: %v", err) } - eb, ok := parsed.(*ExchangeBody) + rb, ok := parsed.(*RFQBody) if !ok { - t.Fatalf("expected *ExchangeBody, got %T", parsed) + t.Fatalf("expected *RFQBody, got %T", parsed) } - if eb.ToAmount != "3000.00" { - t.Errorf("ToAmount: got %q", eb.ToAmount) + if rb.ToAmount != "3000.00" { + t.Errorf("ToAmount: got %q", rb.ToAmount) } } diff --git a/transfer.go b/transfer.go index 4557611..1da8f0b 100644 --- a/transfer.go +++ b/transfer.go @@ -10,16 +10,26 @@ import ( // TransferBody represents the body of a TAP Transfer message (TAIP-3). type TransferBody struct { - Context string `json:"@context"` - Type string `json:"@type"` - Asset string `json:"asset"` - Amount string `json:"amount,omitempty"` - Originator *Party `json:"originator,omitempty"` - Beneficiary *Party `json:"beneficiary,omitempty"` - Agents []Agent `json:"agents"` - SettlementID string `json:"settlementId,omitempty"` - Memo string `json:"memo,omitempty"` - Expiry string `json:"expiry,omitempty"` + Context string `json:"@context"` + Type string `json:"@type"` + Asset string `json:"asset"` + Amount string `json:"amount,omitempty"` + Originator *Party `json:"originator,omitempty"` + Beneficiary *Party `json:"beneficiary,omitempty"` + Agents []Agent `json:"agents"` + SettlementID string `json:"settlementId,omitempty"` + Memo string `json:"memo,omitempty"` + Expiry string `json:"expiry,omitempty"` + TransactionValue *TransactionValue `json:"transactionValue,omitempty"` +} + +// TransactionValue represents the fiat equivalent value of a transfer for compliance +// purposes such as Travel Rule threshold determination (TAIP-3). +type TransactionValue struct { + // Amount is the decimal string representation of the fiat amount. + Amount string `json:"amount"` + // Currency is the ISO 4217 3-letter currency code (e.g. "USD", "EUR"). + Currency string `json:"currency"` } func (b *TransferBody) TAPType() string { return TypeTransfer } diff --git a/transfer_test.go b/transfer_test.go index fab1c16..d4b9ef8 100644 --- a/transfer_test.go +++ b/transfer_test.go @@ -123,6 +123,60 @@ func TestTransferBody_ParseBody(t *testing.T) { } } +func TestTransferBody_TransactionValue(t *testing.T) { + body := &TransferBody{ + Asset: "eip155:1/erc20:0x1234567890abcdef1234567890abcdef12345678", + Amount: "500", + TransactionValue: &TransactionValue{ + Amount: "1250.00", + Currency: "EUR", + }, + Agents: []Agent{{ID: "did:web:originator.vasp", For: NewForField("did:eg:bob")}}, + } + + msg, err := NewTransferMessage("did:web:originator.vasp", []string{"did:web:beneficiary.vasp"}, body) + if err != nil { + t.Fatalf("create: %v", err) + } + + parsed, err := ParseBody(msg) + if err != nil { + t.Fatalf("parse: %v", err) + } + tb := parsed.(*TransferBody) + if tb.TransactionValue == nil { + t.Fatal("TransactionValue: got nil") + } + if tb.TransactionValue.Amount != "1250.00" { + t.Errorf("TransactionValue.Amount: got %q", tb.TransactionValue.Amount) + } + if tb.TransactionValue.Currency != "EUR" { + t.Errorf("TransactionValue.Currency: got %q", tb.TransactionValue.Currency) + } + + // transactionValue is optional — confirm omitempty when nil + bodyNoValue := &TransferBody{ + Asset: "eip155:1/slip44:60", + Agents: []Agent{{ID: "did:web:originator.vasp"}}, + } + msg2, err := NewTransferMessage("did:web:originator.vasp", nil, bodyNoValue) + if err != nil { + t.Fatalf("create: %v", err) + } + if string(msg2.Body) == "" || jsonContains(msg2.Body, "transactionValue") { + t.Errorf("expected no transactionValue field, body=%s", msg2.Body) + } +} + +func jsonContains(body []byte, key string) bool { + for i := 0; i < len(body)-len(key); i++ { + if string(body[i:i+len(key)]) == key { + return true + } + } + return false +} + func TestTransfer_TestVectorValid(t *testing.T) { data, err := os.ReadFile("TAIPs/test-vectors/transfer/valid.json") if err != nil {