├── .github
└── workflows
│ ├── examples-console.yml
│ └── sdk.yml
├── .gitignore
├── Getting Started.md
├── LICENSE
├── README-diagram.png
├── README.md
├── benchmarks
├── Benchmark Report 2021-10-25.md
└── Benchmark Report 2021-11-05.md
├── examples
├── console
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── snapshot.go
│ ├── stats.go
│ └── util.go
└── dashboard.html
├── go.work
├── go.work.sum
├── sdk
├── .golangci.yml
├── agent
│ ├── agent.go
│ ├── agent_test.go
│ ├── agenthttp
│ │ └── agenthttp.go
│ ├── bufferedagent
│ │ ├── agent.go
│ │ ├── buffered_memo.go
│ │ ├── buffered_payment.go
│ │ ├── events.go
│ │ └── tcp.go
│ ├── events.go
│ ├── hash.go
│ ├── horizon
│ │ ├── balance_collector.go
│ │ ├── doc.go
│ │ ├── sequence_number_collector.go
│ │ ├── streamer.go
│ │ ├── streamer_test.go
│ │ └── submitter.go
│ ├── ingest.go
│ ├── msg
│ │ └── msg.go
│ ├── submit
│ │ └── submitter.go
│ └── tcp.go
├── go.mod
├── go.sum
├── state
│ ├── asset.go
│ ├── asset_test.go
│ ├── close.go
│ ├── close_test.go
│ ├── doc.go
│ ├── hash.go
│ ├── hash_test.go
│ ├── ingest.go
│ ├── ingest_test.go
│ ├── integrationtests
│ │ ├── helpers_test.go
│ │ ├── main_test.go
│ │ └── state_test.go
│ ├── open.go
│ ├── open_test.go
│ ├── payment.go
│ ├── payment_test.go
│ ├── state.go
│ ├── state_test.go
│ └── verify.go
└── txbuild
│ ├── close.go
│ ├── close_test.go
│ ├── create_channelaccount.go
│ ├── declaration.go
│ ├── declaration_test.go
│ ├── doc.go
│ ├── open.go
│ ├── sequence.go
│ ├── sequence_test.go
│ └── txbuildtest
│ ├── txbuildtest.go
│ └── txbuildtest_test.go
└── specifications
├── diagrams
├── README.md
├── sequence-diagrams
│ ├── setup-payment-close.txt
│ └── withdraw.txt
└── state-diagrams
│ ├── channel-proposal.txt
│ ├── channel-state.txt
│ └── payment-proposal-threads.txt
└── starlight-payment-channel-mechanics.md
/.github/workflows/examples-console.yml:
--------------------------------------------------------------------------------
1 | name: 'Examples: Console'
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 |
11 | lint:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v2
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: 1
20 | - name: golangci-lint
21 | uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018
22 | with:
23 | version: v1.45.2
24 | working-directory: examples/console
25 | skip-go-installation: true
26 |
27 | build:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v2
32 | - name: Set up Go
33 | uses: actions/setup-go@v2
34 | with:
35 | go-version: 1
36 | - name: Build
37 | working-directory: examples/console
38 | run: go build ./...
39 | - name: Build Tests
40 | working-directory: examples/console
41 | run: go test -exec=echo ./...
42 |
--------------------------------------------------------------------------------
/.github/workflows/sdk.yml:
--------------------------------------------------------------------------------
1 | name: SDK
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 |
11 | lint:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v2
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: 1
20 | - name: golangci-lint
21 | uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018
22 | with:
23 | version: v1.45.2
24 | working-directory: sdk
25 | skip-go-installation: true
26 |
27 | build:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v2
32 | - name: Set up Go
33 | uses: actions/setup-go@v2
34 | with:
35 | go-version: 1
36 | - name: Build SDK
37 | working-directory: sdk
38 | run: go build ./...
39 | - name: Build SDK Tests
40 | working-directory: sdk
41 | run: go test -exec=echo ./...
42 |
43 | unit-tests:
44 | runs-on: ubuntu-latest
45 | steps:
46 | - name: Checkout
47 | uses: actions/checkout@v2
48 | - name: Set up Go
49 | uses: actions/setup-go@v2
50 | with:
51 | go-version: 1
52 | - name: Run SDK Unit Tests
53 | working-directory: sdk
54 | run: go test -v -race ./...
55 |
56 | integration-tests:
57 | runs-on: ubuntu-latest
58 | steps:
59 | - name: "Stellar Quickstart: Run"
60 | run: docker run -d -p 8000:8000 --name stellar stellar/quickstart --standalone --enable-core-artificially-accelerate-time-for-testing
61 | - name: "Stellar Quickstart: Wait for Ready"
62 | run: while ! [ "$(curl -s --fail localhost:8000 | jq '.history_latest_ledger')" -gt 0 ]; do echo waiting; sleep 1; done
63 | - name: "Stellar Quickstart: Details"
64 | run: curl localhost:8000
65 | - name: Checkout
66 | uses: actions/checkout@v2
67 | - name: Set up Go
68 | uses: actions/setup-go@v2
69 | with:
70 | go-version: 1
71 | - name: Run SDK Integration Tests
72 | working-directory: sdk
73 | run: go test -v -race -p=1 ./**/integrationtests
74 | env:
75 | INTEGRATION_TESTS: 1
76 | INTEGRATION_TESTS_HORIZON_URL: http://localhost:8000
77 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.test
3 | *.prof
4 |
--------------------------------------------------------------------------------
/Getting Started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | - [Testing with testnet](#testing-with-testnet)
4 | - [Running your own devnet with CAP-21 and CAP-40](#running-your-own-devnet-with-cap-21-and-cap-40)
5 | - [Experiment with the prototype Starlight Go SDK](#experiment-with-the-prototype-starlight-go-sdk)
6 | - [Run the console example application](#run-the-console-example-application)
7 | - [Manually inspect or build transactions](#manually-inspect-or-build-transactions)
8 |
9 | ## Testing with testnet
10 |
11 | The Stellar testnet has protocol 19 implemented:
12 |
13 | https://horizon-testnet.stellar.org
14 |
15 | ## Running your own devnet with CAP-21 and CAP-40
16 |
17 | To run a standalone network use the branch of the `stellar/quickstart` docker image:
18 |
19 | ### Locally
20 |
21 | ```
22 | docker run --rm -it -p 8000:8000 --name stellar stellar/quickstart:testing --standalone
23 | ```
24 |
25 | Test that the network is running by curling the root endpoint:
26 |
27 | ```
28 | curl http://localhost:8000
29 | ```
30 |
31 | There is a friendbot included in the network that can faucet new accounts.
32 |
33 | ### Deployed
34 |
35 | Alternatively, deploy a standalone network to Digital Ocean to develop and test with:
36 |
37 | [](https://cloud.digitalocean.com/apps/new?repo=https://github.com/stellar/docker-stellar-core-horizon/tree/protocol19)
38 |
39 | The network passphrase will be randomly generated in this case, and you can retrieve it from the logs.
40 |
41 | _Reminder:_ The DigitalOcean server is publicly accessible on the Internet. Do not put sensitive information on the network that you would not want someone else to know. Anyone with access to the network will be able to use the root account above.
42 |
43 | ## Experiment with the prototype Starlight Go SDK
44 |
45 | The `sdk` directory contains the prototype Starlight Go SDK.
46 |
47 | Reference: https://pkg.go.dev/github.com/stellar/starlight/sdk
48 |
49 | ```
50 | go get github.com/stellar/starlight/sdk
51 | ```
52 |
53 | See the [console example](https://github.com/stellar/experimental-payment-channels/tree/readme/examples/console) application for an example for how to use the Starlight agent.
54 |
55 | ## Run the console example application
56 |
57 | The `examples/console` directory contains an example application that operates a payment channel between two participants over a TCP connection. Requires a network to be running with support for CAP-21 and CAP-40.
58 |
59 | See the [README](examples/console/README.md) for more details.
60 |
61 | ## Manually inspect or build transactions
62 |
63 | You can use [stc](https://github.com/xdrpp/stc) to manually build and inspect transactions at the command line using text files.
64 |
65 | Build transactions as you normally would, but for standalone network.
66 | ```
67 | stc -net=standalone -edit tx
68 | stc -net=standalone -sign tx | stc -net=standalone -post -
69 | ```
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stellar-deprecated/starlight/919096552e44eec5ec3fd6278922396926239874/README-diagram.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Creating equitable access to the global financial system
5 |
12 |
13 | Starlight is a prototype layer 2 payment channel protocol for the Stellar Network. Starlight has existed in a couple different forms. The previous version of Starlight lives at [interstellar/starlight](https://github.com/interstellar/starlight).
14 |
15 | **Have a use case for payment channels on Stellar? Tell us about it [here](https://github.com/stellar/starlight/discussions/new?category=use-cases).**
16 |
17 | This repository contains a experiments, prototypes, documents, and issues
18 | relating to implementing the Starlight protocol on the Stellar network.
19 | Protoypes here are dependent on Protocol 19 and Core Advancement Protocols,
20 | [CAP-21] and [CAP-40]. You can experiment with the Starlight protocol by using
21 | the testnet, or running a Stellar network in a docker container. To find out
22 | how, see [Getting Started](Getting%20Started.md).
23 |
24 | 
25 |
26 | The Starlight protocol, SDK, code in this repository, and any forks of other Stellar software referenced here, are **experimental** and **not recommended for use in production** systems. Please use the SDK to experiment with payment channels on Stellar, but it is not recommended for use with assets that hold real world value.
27 |
28 | ## Try it out
29 |
30 | Run the example console application with testnet:
31 |
32 | ```
33 | git clone https://github.com/stellar/starlight
34 | cd examples/console
35 | go run .
36 | >>> help
37 | ```
38 |
39 | Run two copies of the example console application and connect them directly over
40 | TCP to open a payment channel between two participants.
41 | More details in the [README](https://github.com/stellar/starlight/tree/main/examples/console).
42 |
43 | ## Get involved
44 |
45 | - [Discord](https://discord.gg/xGWRjyNzQh)
46 | - [Discussions](https://github.com/stellar/starlight/discussions)
47 | - [Demos](https://github.com/stellar/starlight/discussions/categories/demos)
48 | - [Getting Started](Getting%20Started.md)
49 | - [Specifications](specifications/)
50 | - [Benchmarks](benchmarks/)
51 |
52 | ## Build and experiment
53 |
54 | - [SDK](https://pkg.go.dev/github.com/stellar/starlight/sdk)
55 | - [Examples](examples/)
56 |
57 | ## Discussions
58 |
59 | - Live chat is on the `#starlight` channel in [Discord](https://discord.gg/xGWRjyNzQh)
60 | - Discussions about Starlight are on [GitHub Discussions](https://github.com/stellar/starlight/discussions)
61 |
62 | ## Protocol 19
63 |
64 | The code in this repository is dependent on changes to the Stellar protocol coming in Protocol 19, specifically the changes described by [CAP-21] and [CAP-40]. Protocol 19 is released.
65 |
66 | [CAP-21]: https://stellar.org/protocol/cap-21
67 | [CAP-40]: https://stellar.org/protocol/cap-40
68 |
--------------------------------------------------------------------------------
/benchmarks/Benchmark Report 2021-10-25.md:
--------------------------------------------------------------------------------
1 | # Benchmark Report 2021-10-25
2 |
3 | This report is a summary of the results seen on 2021-10-25 and 2021-10-26
4 | benchmarking a Starlight payment channel. This report details the setup and how
5 | the benchmark was run, as well as the results.
6 |
7 | The application used for the benchmarking is the `examples/console` application
8 | in this repository. Some aspects of the application have been optimized, but
9 | many parts have not. As an example, participants use simple JSON where some of
10 | the message is compressed. The results in this report should not be viewed as
11 | the maximum possible performance possible with a Starlight payment channel.
12 |
13 | ## Summary
14 |
15 | The test demonstrated that performance is influenced significantly by the
16 | latency of the network connection between the payment channel participants.
17 | However, even connections with greater latency, such as those spanning
18 | countries, can deliver high numbers of payments per second.
19 |
20 | The tests use a Starlight bi-directional payment channel, but only move assets
21 | in a single direction for the duration of the test. The tests send payments
22 | using buffering, where payments are buffered and sent in batches.
23 |
24 | Test AB6 demonstrated over 400,000 payments per second for payments varying from
25 | 0.0000001 to 1000.0, over Internet with 10-20ms latency.
26 |
27 | Since smaller payment amounts take up less data, higher payments per second were
28 | observed for payments that trend towards micro-payments.
29 |
30 | For example, test AB5 demonstrated over 700,000 payments per second for payments
31 | varying from 0.0000001 to 1.0. And, test AB2 over 1,190,000 payments per second
32 | with micro-payments where every payment is for 0.0000001.
33 |
34 | Tests AC1, AC2, AC3 also demonstrated that at least 17,000 to 130,000 payments
35 | per second is possible on networks with latency as high as 215ms.
36 |
37 | ## Participants
38 |
39 | Three participants were involved in the tests, with a payment channel formed
40 | between participants A and B, and also between participants A and C.
41 |
42 | ### Participant A
43 |
44 | - Location: San Francisco, US
45 | - Network: 1gbps
46 | - Hardware:
47 | ```
48 | MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports)
49 | 2.8 GHz Quad-Core Intel Core i7
50 | 16 GB 2133 MHz LPDDR3
51 | ```
52 |
53 | ### Participant B
54 |
55 | - Location: San Francisco, US
56 | - Network: ~200mbps
57 | - Hardware:
58 | ```
59 | MacBook Pro (16-inch, 2019)
60 | 2.6 GHz 6-Core Intel Core i7
61 | 32 GB 2667 MHz DDR4
62 | ```
63 |
64 | ### Participant C
65 |
66 | - Location: Florianopolis, Brazil
67 | - Network: 200mbps
68 | - Hardware:
69 | ```
70 | MacBook Air (M1, 2020)
71 | Apple M1
72 | 16 GB
73 | ```
74 |
75 | ## Setup
76 |
77 | A preivate network running using Docker container from:
78 | https://github.com/stellar/docker-stellar-core-horizon/tree/4d34da83d0876e21ff68cf90653053879239707b
79 |
80 | Benchmark application in use `examples/console` from:
81 | https://github.com/stellar/starlight/tree/f43363c6b1593134344629b32883529f494bab23
82 |
83 | Note in both setups below no snapshots of the channel state are being written to
84 | disk. Data persistence functionality exists in the console application and can
85 | be enabled, but was not enabled for these tests. However, no significant
86 | difference in performance has been witnessed with having that functionality
87 | enabled.
88 |
89 | ### Listener
90 |
91 | ```
92 | $ cd examples/console
93 | $ go run -horizon= -listen=:8001
94 | ```
95 |
96 | ### Connector
97 |
98 | ```
99 | $ cd examples/console
100 | $ go run -horizon= -http=9000 -connect=:8001
101 | > open USD
102 | > deposit 10000000
103 | > deposit 10000000 other
104 | ```
105 |
106 | ## Tests and Results
107 |
108 | ### Understanding the Tests
109 |
110 | Each test sends payments in one direction on the bi-directional payment channel.
111 | Each test specifies a max payment amount, payment count, and a maximum buffer
112 | size in the `payx` command. i.e. `payx
113 | `.
114 |
115 | For example, test AB6 `payx 1000 1000000 50000`, sent 1 million payments, with
116 | amounts varying from 0.0000001 to 1000.0, and it allowed payments to buffer into
117 | batches holding up to 50k payments.
118 |
119 | Payments are buffered until the channel is available to propose the next
120 | agreement. An agreement is then built containing one or more buffered payments.
121 | The agreement containing a list of all buffered payments are transmitted to the
122 | recipient, and back again in full.
123 |
124 | Agreements TPS is the number of agreements per second that the participants have
125 | signed and exchanged. Agreements are transmitted across the wire and so the
126 | agreements TPS is limited by the patency of both participants and their
127 | capability to quickly produce ed25519 signatures for the two transactions that
128 | form each agreement. Agreements TPS is limited by the latency between both
129 | participants. For example, if the participants have a ping time of 30ms, the
130 | highest agreements TPS value possible will be 1s/30ms=33tps. Or if the
131 | participants have a ping time of 200ms, the highest agreements TPS value
132 | possible will be 1s/200ms=5tps.
133 |
134 | Buffered payments TPS is the number of buffered payments per second that the
135 | participants have exchanged inside agreements. Buffered payments are transmitted
136 | across the wire inside an agreement. The maximum buffer size is the maximum
137 | number of payments that will be transmitted in a single agreement. Buffered
138 | payments TPS are limited by the bandwidth and latency between both participants.
139 |
140 | Buffered payments average buffer size is the average size in bytes of the
141 | buffers that were transmitted between participants.
142 |
143 | For more information on buffering payments, see this discussion: https://github.com/stellar/starlight/discussions/330#discussioncomment-1345257
144 |
145 | #### Participants A (US) and B (US)
146 |
147 | Latency between participants A and B was observed to be approximately 10-30ms.
148 |
149 | ##### Test AB1
150 |
151 | ```
152 | > payx 0.0000001 3000000 15000
153 | sending 0.0000001 payment 3000000 times
154 | time spent: 4.063101492s
155 | agreements sent: 201
156 | agreements received: 0
157 | agreements tps: 49.470
158 | buffered payments sent: 3000000
159 | buffered payments received: 0
160 | buffered payments tps: 738352.218
161 | buffered payments max buffer size: 55072
162 | buffered payments min buffer size: 600
163 | buffered payments avg buffer size: 48020
164 | ```
165 |
166 | ##### Test AB2
167 |
168 | ```
169 | > payx 0.0000001 3000000 95000
170 | sending 0.0000001 payment 3000000 times
171 | time spent: 2.50396729s
172 | agreements sent: 33
173 | agreements received: 0
174 | agreements tps: 13.179
175 | buffered payments sent: 3000000
176 | buffered payments received: 0
177 | buffered payments tps: 1198098.718
178 | buffered payments max buffer size: 349356
179 | buffered payments min buffer size: 13380
180 | buffered payments avg buffer size: 234642
181 | ```
182 |
183 | ##### Test AB3
184 |
185 | ```
186 | > payx 1 3000000 15000
187 | sending 1 payment 3000000 times
188 | time spent: 5.898862686s
189 | agreements sent: 201
190 | agreements received: 0
191 | agreements tps: 34.074
192 | buffered payments sent: 3000000
193 | buffered payments received: 0
194 | buffered payments tps: 508572.611
195 | buffered payments max buffer size: 153684
196 | buffered payments min buffer size: 1980
197 | buffered payments avg buffer size: 152424
198 | ```
199 |
200 | ##### Test AB4
201 |
202 | ```
203 | > payx 1 3000000 1000
204 | sending 1 payment 3000000 times
205 | time spent: 39.129140064s
206 | agreements sent: 3001
207 | agreements received: 0
208 | agreements tps: 76.695
209 | buffered payments sent: 3000000
210 | buffered payments received: 0
211 | buffered payments tps: 76669.203
212 | buffered payments max buffer size: 10292
213 | buffered payments min buffer size: 1140
214 | buffered payments avg buffer size: 9601
215 | ```
216 |
217 | ##### Test AB5
218 |
219 | ```
220 | > payx 1 1000000 50000
221 | sending 1 payment 1000000 times
222 | time spent: 1.427511708s
223 | agreements sent: 21
224 | agreements received: 0
225 | agreements tps: 14.711
226 | buffered payments sent: 1000000
227 | buffered payments received: 0
228 | buffered payments tps: 700519.649
229 | buffered payments max buffer size: 503908
230 | buffered payments min buffer size: 1992
231 | buffered payments avg buffer size: 502620
232 | ```
233 |
234 | ##### Test AB6
235 |
236 | ```
237 | > payx 1000 1000000 50000
238 | sending 1000 payment 1000000 times
239 | time spent: 2.421638555s
240 | agreements sent: 21
241 | agreements received: 0
242 | agreements tps: 8.672
243 | buffered payments sent: 1000000
244 | buffered payments received: 0
245 | buffered payments tps: 412943.541
246 | buffered payments max buffer size: 619884
247 | buffered payments min buffer size: 138768
248 | buffered payments avg buffer size: 549220
249 | ```
250 |
251 | #### Participants A (US) and C (Brazil)
252 |
253 | Latency between participants A and C was observed to be approximately 215ms.
254 |
255 | ##### Test AC1
256 |
257 | ```
258 | > payx 0.0000001 3000000 15000
259 | sending 0.0000001 payment 3000000 times
260 | time spent: 1m28.168218631s
261 | agreements sent: 201
262 | agreements received: 0
263 | agreements tps: 2.280
264 | buffered payments sent: 3000000
265 | buffered payments received: 0
266 | buffered payments tps: 34025.866
267 | buffered payments max buffer size: 55144
268 | buffered payments min buffer size: 1520
269 | buffered payments avg buffer size: 47475
270 | ```
271 |
272 | ##### Test AC2
273 |
274 | ```
275 | > payx 0.0000001 3000000 95000
276 | sending 0.0000001 payment 3000000 times
277 | time spent: 22.827299992s
278 | agreements sent: 33
279 | agreements received: 0
280 | agreements tps: 1.446
281 | buffered payments sent: 3000000
282 | buffered payments received: 0
283 | buffered payments tps: 131421.587
284 | buffered payments max buffer size: 349288
285 | buffered payments min buffer size: 952
286 | buffered payments avg buffer size: 240436
287 | ```
288 |
289 | ##### Test AC3
290 |
291 | ```
292 | > payx 1 3000000 15000
293 | sending 1 payment 3000000 times
294 | time spent: 2m47.082713s
295 | agreements sent: 201
296 | agreements received: 0
297 | agreements tps: 1.203
298 | buffered payments sent: 3000000
299 | buffered payments received: 0
300 | buffered payments tps: 17955.179
301 | buffered payments max buffer size: 153592
302 | buffered payments min buffer size: 1648
303 | buffered payments avg buffer size: 152538
304 | ```
305 |
306 | ##### Test AC4
307 |
308 | ```
309 | > payx 1 3000000 1000
310 | sending 1 payment 3000000 times
311 | time spent: 12m37.733772304s
312 | agreements sent: 3001
313 | agreements received: 0
314 | agreements tps: 3.960
315 | buffered payments sent: 3000000
316 | buffered payments received: 0
317 | buffered payments tps: 3959.174
318 | buffered payments max buffer size: 10484
319 | buffered payments min buffer size: 3376
320 | buffered payments avg buffer size: 6816
321 | ```
322 |
--------------------------------------------------------------------------------
/benchmarks/Benchmark Report 2021-11-05.md:
--------------------------------------------------------------------------------
1 | # Benchmark Report 2021-11-05
2 |
3 | This report is a summary of the results seen on 2021-11-05 benchmarking a
4 | Starlight payment channel. This report details the setup and how the benchmark
5 | was run, as well as the results.
6 |
7 | The application used for the benchmarking is the `examples/console` application
8 | in this repository. Some aspects of the application have been optimized, but
9 | many parts have not. The results in this report should not be viewed as the
10 | maximum possible performance possible with a Starlight payment channel.
11 |
12 | ## Summary
13 |
14 | The tests use a Starlight bi-directional payment channel, but only move assets
15 | in a single direction for the duration of the test. The tests send payments
16 | using buffering, where payments are buffered and sent in batches.
17 |
18 | Test AB1 enabled snapshotting and demonstrated 1.1 million payments per second
19 | for payments varying from 0.0000001 to 0.001.
20 |
21 | Test AB2 disabled snapshotting and demonstrated similar, over 1.5 million
22 | payments per second for payments varying from 0.0000001 to 0.001.
23 |
24 | Test AB3 demonstrated over 1.5 million payments per second for payments varying
25 | from 1 to 1000.0, over Internet with 20-30ms latency.
26 |
27 | Test AB4 sent payments serially and was therefore limited by the network latency
28 | between participants and demonstrated 38 payments and agreements per second.
29 |
30 | An impact on performance was witnessed when enabling snapshotting as both
31 | participants wrote the entire state of the channel to a JSON file. The
32 | application writes the file and blocks until that is complete to ensure the
33 | state of the channel is persisted before continuing and this briefly delays the
34 | sending of the next payment.
35 |
36 | There was no noticeable difference in buffer size and performance with different
37 | size payments during these tests even though smaller amounts do result in
38 | smaller buffers before they're compressed. This is because amounts are encoded
39 | as variable binary integers into the minimum number of bytes required to hold
40 | the value. The lack of difference in the final buffer size may be due to GZIP
41 | compression that is used to reduce the size of the buffers.
42 |
43 | ## Participants
44 |
45 | Two participants were involved in the tests, with a payment channel formed
46 | between participants A and B.
47 |
48 | ### Participant A
49 |
50 | - Location: San Francisco, US
51 | - Network: 1gbps
52 | - Hardware:
53 | ```
54 | MacBook Pro (13-inch, 2019, Four Thunderbolt 3 ports)
55 | 2.8 GHz Quad-Core Intel Core i7
56 | 16 GB 2133 MHz LPDDR3
57 | ```
58 |
59 | ### Participant B
60 |
61 | - Location: San Francisco, US
62 | - Network: ~200mbps
63 | - Hardware:
64 | ```
65 | MacBook Pro (16-inch, 2019)
66 | 2.6 GHz 6-Core Intel Core i7
67 | 32 GB 2667 MHz DDR4
68 | ```
69 |
70 | ## Setup
71 |
72 | A private network running using Docker container from:
73 | https://github.com/stellar/docker-stellar-core-horizon/tree/4d34da83d0876e21ff68cf90653053879239707b
74 |
75 | Benchmark application in use `examples/console` from:
76 | https://github.com/stellar/starlight/tree/9dbede9133883d6fcd69ca037c3e3466bad1f974
77 |
78 | Note in some tests no snapshots of the channel state are being written to disk.
79 | See each test for whether it was enabled or not. Tests that had it enabled used
80 | the `-f` option included as optional below.
81 |
82 | ### Listener
83 |
84 | ```
85 | $ cd examples/console
86 | $ go run -horizon= -listen=:8001 [-f snapshot.json]
87 | ```
88 |
89 | ### Connector
90 |
91 | ```
92 | $ cd examples/console
93 | $ go run -horizon= -connect=:8001 [-f snapshot.json]
94 | > open USD
95 | > deposit 90000000000
96 | ```
97 |
98 | ## Tests and Results
99 |
100 | ### Understanding the Tests
101 |
102 | Each test sends payments in one direction on the bi-directional payment channel.
103 | Each test specifies a max payment amount, payment count, and a maximum buffer
104 | size in the `payx` command. i.e. `payx
105 | `.
106 |
107 | For example, test AB1 `payx 0.001 10000000 95000`, sent 1 million payments, with
108 | amounts varying from 0.00000001 to 0.001, and it allowed payments to buffer into
109 | batches holding up to 95k payments.
110 |
111 | Payments are buffered until the channel is available to propose the next
112 | agreement. An agreement is then built containing one or more buffered payments.
113 | The agreement containing a list of all buffered payments are transmitted to the
114 | recipient, and back again in full.
115 |
116 | Agreements TPS is the number of agreements per second that the participants have
117 | signed and exchanged. Agreements are transmitted across the wire and so the
118 | agreements TPS is limited by the latency between both participants and their
119 | capability to quickly produce ed25519 signatures for the two transactions that
120 | form each agreement. For example, if the participants have a ping time of 30ms,
121 | the highest agreements TPS value possible will be 1s/30ms=33tps.
122 |
123 | Buffered payments TPS is the number of buffered payments per second that the
124 | participants have exchanged inside agreements. Buffered payments are transmitted
125 | across the wire inside an agreement. The maximum buffer size is the maximum
126 | number of payments that will be transmitted in a single agreement. Buffered
127 | payments TPS are limited by the bandwidth and latency between both participants.
128 |
129 | Buffered payments average buffer size is the average size in bytes of the
130 | buffers that were transmitted between participants.
131 |
132 | For more information on buffering payments, see this discussion:
133 | https://github.com/stellar/starlight/discussions/330#discussioncomment-1345257
134 |
135 | #### Participants A (US) and B (US)
136 |
137 | Latency between participants A and B was observed to be approximately 20-30ms.
138 |
139 | ##### Test AB1
140 |
141 | Test AB1 ran 10 million payments of $0.001 or less, with payments being buffered
142 | into buffers of max size 95,000. The application blocked and waited for the
143 | response of the previous buffer before sending the next buffer. 12 buffers per
144 | second and 1.1 million payments per second were witnessed. Each message was a
145 | bit more than 309KB in size. Snapshots were enabled and as such the state of the
146 | channel was being written to disk.
147 |
148 | ```
149 | >>> payx 0.001 10000000 95000
150 | sending 0.001 payment 10000000 times
151 | time spent: 8.402299672s
152 | agreements sent: 107
153 | agreements received: 0
154 | agreements tps: 12.735
155 | buffered payments sent: 10000000
156 | buffered payments received: 0
157 | buffered payments tps: 1190150.362
158 | buffered payments max buffer size: 490698
159 | buffered payments min buffer size: 773
160 | buffered payments avg buffer size: 309420
161 | ```
162 |
163 | ##### Test AB3
164 |
165 | Test AB3 ran 10 million payments of $0.001 or less, with payments being buffered
166 | into buffers of max size 95,000. The application blocked and waited for the
167 | response of the previous buffer before sending the next buffer. 16 buffers per
168 | second and 1.5 million payments per second were witnessed. Each message was a
169 | bit more than 490KB in size. Snapshots were not enabled and as such the state of
170 | the channel was not being written to disk.
171 |
172 | ```
173 | >>> payx 0.001 10000000 95000
174 | sending 0.001 payment 10000000 times
175 | time spent: 6.429692082s
176 | agreements sent: 107
177 | agreements received: 0
178 | agreements tps: 16.642
179 | buffered payments sent: 10000000
180 | buffered payments received: 0
181 | buffered payments tps: 1555284.432
182 | buffered payments max buffer size: 490652
183 | buffered payments min buffer size: 544
184 | buffered payments avg buffer size: 309526
185 | ```
186 |
187 | ##### Test AB3
188 |
189 | Test AB3 ran 10 million payments of $1000 or less, with payments being buffered
190 | into buffers of max size 95,000. The application blocked and waited for the
191 | response of the previous buffer before sending the next buffer. 16 buffers per
192 | second and 1.5 million payments per second were witnessed. Each message was a
193 | bit more than 485KB in size. Snapshots were not enabled and as such the state of
194 | the channel was not being written to disk.
195 |
196 | ```
197 | >>> payx 1000 10000000 95000
198 | sending 1000 payment 10000000 times
199 | time spent: 6.444609094s
200 | agreements sent: 107
201 | agreements received: 0
202 | agreements tps: 16.603
203 | buffered payments sent: 10000000
204 | buffered payments received: 0
205 | buffered payments tps: 1551684.494
206 | buffered payments max buffer size: 485915
207 | buffered payments min buffer size: 768
208 | buffered payments avg buffer size: 306113
209 | ```
210 |
211 | ##### Test AB4
212 |
213 | Test AB4 ran 1000 payments of $1 or less, with each payment being sent serially
214 | over the network. The application blocked and waited for the response before
215 | sending the next payment. 38 payments per second were witnessed. Each message
216 | was a bit more than 190 bytes in size. Snapshots were not enabled and as such
217 | the state of the channel was not being written to disk.
218 |
219 | ```
220 | >>> payx 1 1000 1
221 | sending 1 payment 1000 times
222 | time spent: 26.247297693s
223 | agreements sent: 1000
224 | agreements received: 0
225 | agreements tps: 38.099
226 | buffered payments sent: 1000
227 | buffered payments received: 0
228 | buffered payments tps: 38.099
229 | buffered payments max buffer size: 190
230 | buffered payments min buffer size: 182
231 | buffered payments avg buffer size: 188
232 | ```
233 |
--------------------------------------------------------------------------------
/examples/console/README.md:
--------------------------------------------------------------------------------
1 | # Console Example
2 |
3 | This module contains an example using the SDK in this repository that supports a
4 | single payment channel controlled by two people through a command line interface
5 | using simple TCP network connection and JSON messages.
6 |
7 | ## State
8 |
9 | This example is a rough example and is brittle and is still a work in progress.
10 |
11 | ## Usage
12 |
13 | Run the example using the below commands.
14 |
15 | ```
16 | git clone https://github.com/stellar/starlight
17 | cd examples/console
18 | go run .
19 | ```
20 |
21 | Type `help` once in to discover commands to use.
22 |
23 | Foreground events and commands are written to stdout. Background events are
24 | written to stderr. To view them side-by-side pipe them to a file or alternative
25 | tty device.
26 |
27 | ## Example
28 |
29 | Run these commands on the first terminal:
30 | ```
31 | $ go run .
32 | > listen :6000
33 | > open
34 | > deposit 100
35 | > pay 4
36 | > pay 46
37 | ```
38 |
39 | Run these commands in the second terminal:
40 | ```
41 | $ go run .
42 | > connect :6000
43 | > deposit 80
44 | > pay 2
45 | > declareclose
46 | > close
47 | ```
48 |
--------------------------------------------------------------------------------
/examples/console/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/stellar/starlight/examples/console
2 |
3 | go 1.18
4 |
5 | replace github.com/stellar/starlight/sdk => ../../sdk
6 |
7 | require (
8 | github.com/abiosoft/ishell v2.0.0+incompatible
9 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00
10 | github.com/stellar/go v0.0.0-20220406183204-45b6f52202f3
11 | github.com/stellar/starlight/sdk v0.0.0-00010101000000-000000000000
12 | )
13 |
14 | require (
15 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
16 | github.com/chzyer/logex v1.1.10 // indirect
17 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/fatih/color v1.13.0 // indirect
20 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
21 | github.com/go-chi/chi v4.0.3+incompatible // indirect
22 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 // indirect
23 | github.com/google/uuid v1.2.0 // indirect
24 | github.com/gorilla/schema v1.1.0 // indirect
25 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd // indirect
26 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc // indirect
27 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect
28 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
29 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
30 | github.com/mattn/go-colorable v0.1.9 // indirect
31 | github.com/mattn/go-isatty v0.0.14 // indirect
32 | github.com/pkg/errors v0.9.1 // indirect
33 | github.com/pmezard/go-difflib v1.0.0 // indirect
34 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 // indirect
35 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect
36 | github.com/sirupsen/logrus v1.4.1 // indirect
37 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee // indirect
38 | github.com/stretchr/objx v0.3.0 // indirect
39 | github.com/stretchr/testify v1.7.0 // indirect
40 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
41 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
42 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
43 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
44 | )
45 |
--------------------------------------------------------------------------------
/examples/console/go.sum:
--------------------------------------------------------------------------------
1 | github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
2 | github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
3 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
4 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
5 | github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM=
6 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
7 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
14 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
15 | github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU=
16 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
17 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
18 | github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4=
19 | github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
20 | github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
21 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 h1:jxTbmDuqQUTI6MscgbqB39vtxGfr2fi61nYIcFQUnlE=
22 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
23 | github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU=
24 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
25 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
26 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
27 | github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
28 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
29 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
30 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
31 | github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 h1:Aw95BEvxJ3K6o9GGv5ppCd1P8hkeIeEJ30FO+OhOJpM=
32 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd h1:vQ0EEfHpdFUtNRj1ri25MUq5jb3Vma+kKhLyjeUTVow=
33 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
34 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc h1:WW8B7p7QBnFlqRVv/k6ro/S8Z7tCnYjJHcQNScx9YVs=
35 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
36 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 h1:KAZ1BW2TCmT6PRihDPpocIy1QTtsAsrx6TneU/4+CMg=
37 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
38 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
39 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
40 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
41 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
42 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
43 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM=
44 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
45 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
46 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
47 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
48 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
49 | github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ=
50 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
51 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
52 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
53 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
54 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
55 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
56 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 h1:8DPul/X0IT/1TNMIxoKLwdemEOBBHDC/K4EB16Cw5WE=
57 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
58 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 h1:3hxavr+IHMsQBrYUPQM5v0CgENFktkkbg1sfpgM3h20=
59 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
60 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc=
61 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y=
62 | github.com/sergi/go-diff v0.0.0-20161205080420-83532ca1c1ca h1:oR/RycYTFTVXzND5r4FdsvbnBn0HJXSVeNAnwaTXRwk=
63 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
64 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
65 | github.com/stellar/go v0.0.0-20220406183204-45b6f52202f3 h1:PLVF895y1tGeDj1L2Wzocl6Vn60zYCSAKtuATczbLYc=
66 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee h1:fbVs0xmXpBvVS4GBeiRmAE3Le70ofAqFMch1GTiq/e8=
67 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps=
68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
69 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
70 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
71 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
72 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
74 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
75 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
76 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
77 | github.com/valyala/fasthttp v0.0.0-20170109085056-0a7f0a797cd6 h1:s0IDmR1jFyWvOK7jVIuAsmHQaGkXUuTas8NXFUOwuAI=
78 | github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc=
79 | github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 h1:KM4T3G70MiR+JtqplcYkNVoNz7pDwYaBxWBXQK804So=
80 | github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c h1:XZWnr3bsDQWAZg4Ne+cPoXRPILrNlPNQfxBuwLl43is=
81 | github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce h1:cVSRGH8cOveJNwFEEZLXtB+XMnRqKLjUP6V/ZFYQCXI=
82 | github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb h1:06WAhQa+mYv7BiOk13B/ywyTlkoE/S7uu6TBKU6FHnE=
83 | github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
84 | github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce h1:888GrqRxabUce7lj4OaoShPxodm3kXOMpSa85wdYzfY=
85 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
86 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
87 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
88 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
89 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
90 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
91 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
92 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
93 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
95 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
96 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
97 | gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw=
98 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
99 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
101 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
102 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103 |
--------------------------------------------------------------------------------
/examples/console/snapshot.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "time"
7 |
8 | "github.com/stellar/go/keypair"
9 | agentpkg "github.com/stellar/starlight/sdk/agent"
10 | )
11 |
12 | type File struct {
13 | ObservationPeriodTime time.Duration
14 | ObservationPeriodLedgerGap uint32
15 | MaxOpenExpiry time.Duration
16 | ChannelAccountKey *keypair.FromAddress
17 | Snapshot agentpkg.Snapshot
18 | }
19 |
20 | type JSONFileSnapshotter struct {
21 | Filename string
22 |
23 | ObservationPeriodTime time.Duration
24 | ObservationPeriodLedgerGap uint32
25 | MaxOpenExpiry time.Duration
26 | ChannelAccountKey *keypair.FromAddress
27 | }
28 |
29 | func (j JSONFileSnapshotter) Snapshot(a *agentpkg.Agent, s agentpkg.Snapshot) {
30 | f := File{
31 | ObservationPeriodTime: j.ObservationPeriodTime,
32 | ObservationPeriodLedgerGap: j.ObservationPeriodLedgerGap,
33 | MaxOpenExpiry: j.MaxOpenExpiry,
34 | ChannelAccountKey: j.ChannelAccountKey,
35 | Snapshot: s,
36 | }
37 | b, err := json.MarshalIndent(f, "", " ")
38 | if err != nil {
39 | panic(err)
40 | }
41 | err = ioutil.WriteFile(j.Filename, b, 0644)
42 | if err != nil {
43 | panic(err)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/examples/console/stats.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "math"
7 | "strings"
8 | "sync"
9 | "time"
10 | )
11 |
12 | type stats struct {
13 | mu sync.RWMutex
14 | timeStart time.Time
15 | timeFinish time.Time
16 | agreementsSent int64
17 | agreementsReceived int64
18 | bufferedPaymentsSent int64
19 | bufferedPaymentsReceived int64
20 | maxBufferByteSize int
21 | minBufferByteSize int
22 | avgBufferByteSize int
23 | }
24 |
25 | func (s *stats) Reset() {
26 | s.mu.Lock()
27 | defer s.mu.Unlock()
28 | s.timeStart = time.Time{}
29 | s.timeFinish = time.Time{}
30 | s.agreementsSent = 0
31 | s.agreementsReceived = 0
32 | s.bufferedPaymentsSent = 0
33 | s.bufferedPaymentsReceived = 0
34 | s.maxBufferByteSize = 0
35 | s.minBufferByteSize = 0
36 | s.avgBufferByteSize = 0
37 | }
38 |
39 | func (s *stats) Clone() *stats {
40 | s.mu.RLock()
41 | defer s.mu.RUnlock()
42 | return &stats{
43 | agreementsSent: s.agreementsSent,
44 | agreementsReceived: s.agreementsReceived,
45 | bufferedPaymentsSent: s.bufferedPaymentsSent,
46 | bufferedPaymentsReceived: s.bufferedPaymentsReceived,
47 | maxBufferByteSize: s.maxBufferByteSize,
48 | minBufferByteSize: s.minBufferByteSize,
49 | }
50 | }
51 |
52 | func (s *stats) MarkStart() {
53 | s.mu.Lock()
54 | defer s.mu.Unlock()
55 | if !s.timeStart.IsZero() {
56 | panic("marking start of stats when already marked")
57 | }
58 | s.timeStart = time.Now()
59 | }
60 |
61 | func (s *stats) MarkFinish() {
62 | s.mu.Lock()
63 | defer s.mu.Unlock()
64 | if !s.timeFinish.IsZero() {
65 | panic("marking finish of stats when already marked")
66 | }
67 | s.timeFinish = time.Now()
68 | }
69 |
70 | func (s *stats) AddAgreementsSent(delta int) {
71 | s.mu.Lock()
72 | defer s.mu.Unlock()
73 | s.agreementsSent += int64(delta)
74 | }
75 |
76 | func (s *stats) AddAgreementsReceived(delta int) {
77 | s.mu.Lock()
78 | defer s.mu.Unlock()
79 | s.agreementsReceived += int64(delta)
80 | }
81 |
82 | func (s *stats) AddBufferedPaymentsSent(delta int) {
83 | s.mu.Lock()
84 | defer s.mu.Unlock()
85 | s.bufferedPaymentsSent += int64(delta)
86 | }
87 |
88 | func (s *stats) AddBufferedPaymentsReceived(delta int) {
89 | s.mu.Lock()
90 | defer s.mu.Unlock()
91 | s.bufferedPaymentsReceived += int64(delta)
92 | }
93 |
94 | func (s *stats) AddBufferByteSize(size int) {
95 | s.mu.Lock()
96 | defer s.mu.Unlock()
97 | if size > s.maxBufferByteSize {
98 | s.maxBufferByteSize = size
99 | }
100 | if s.minBufferByteSize == 0 || size < s.minBufferByteSize {
101 | s.minBufferByteSize = size
102 | }
103 | s.avgBufferByteSize = (s.avgBufferByteSize + size) / 2
104 | }
105 |
106 | func (s *stats) AgreementsPerSecond() float64 {
107 | timeFinish := s.timeFinish
108 | if timeFinish.IsZero() {
109 | timeFinish = time.Now()
110 | }
111 | duration := s.timeFinish.Sub(s.timeStart)
112 | rate := float64(s.agreementsSent+s.agreementsReceived) / duration.Seconds()
113 | if math.IsNaN(rate) || math.IsInf(rate, 0) {
114 | rate = 0
115 | }
116 | return rate
117 | }
118 |
119 | func (s *stats) BufferedPaymentsPerSecond() float64 {
120 | timeFinish := s.timeFinish
121 | if timeFinish.IsZero() {
122 | timeFinish = time.Now()
123 | }
124 | duration := timeFinish.Sub(s.timeStart)
125 | rate := float64(s.bufferedPaymentsSent+s.bufferedPaymentsReceived) / duration.Seconds()
126 | if math.IsNaN(rate) || math.IsInf(rate, 0) {
127 | rate = 0
128 | }
129 | return rate
130 | }
131 |
132 | func (s *stats) Summary() string {
133 | s.mu.RLock()
134 | defer s.mu.RUnlock()
135 | sb := strings.Builder{}
136 | timeFinish := s.timeFinish
137 | if timeFinish.IsZero() {
138 | timeFinish = time.Now()
139 | }
140 | duration := timeFinish.Sub(s.timeStart)
141 | fmt.Fprintf(&sb, "time spent: %v\n", duration)
142 | fmt.Fprintf(&sb, "agreements sent: %d\n", s.agreementsSent)
143 | fmt.Fprintf(&sb, "agreements received: %d\n", s.agreementsReceived)
144 | fmt.Fprintf(&sb, "agreements tps: %.3f\n", s.AgreementsPerSecond())
145 | fmt.Fprintf(&sb, "buffered payments sent: %d\n", s.bufferedPaymentsSent)
146 | fmt.Fprintf(&sb, "buffered payments received: %d\n", s.bufferedPaymentsReceived)
147 | fmt.Fprintf(&sb, "buffered payments tps: %.3f\n", s.BufferedPaymentsPerSecond())
148 | fmt.Fprintf(&sb, "buffered payments max buffer size: %d\n", s.maxBufferByteSize)
149 | fmt.Fprintf(&sb, "buffered payments min buffer size: %d\n", s.minBufferByteSize)
150 | fmt.Fprintf(&sb, "buffered payments avg buffer size: %d\n", s.avgBufferByteSize)
151 | return sb.String()
152 | }
153 |
154 | func (s *stats) MarshalJSON() ([]byte, error) {
155 | s.mu.RLock()
156 | defer s.mu.RUnlock()
157 | v := struct {
158 | AgreementsSent int64
159 | AgreementsReceived int64
160 | AgreementsPerSecond int64
161 | BufferedPaymentsSent int64
162 | BufferedPaymentsReceived int64
163 | BufferedPaymentsPerSecond int64
164 | MaxBufferByteSize int
165 | MinBufferByteSize int
166 | AvgBufferByteSize int
167 | }{
168 | AgreementsSent: s.agreementsSent,
169 | AgreementsReceived: s.agreementsReceived,
170 | AgreementsPerSecond: int64(s.AgreementsPerSecond()),
171 | BufferedPaymentsSent: s.bufferedPaymentsSent,
172 | BufferedPaymentsReceived: s.bufferedPaymentsReceived,
173 | BufferedPaymentsPerSecond: int64(s.BufferedPaymentsPerSecond()),
174 | MaxBufferByteSize: s.maxBufferByteSize,
175 | MinBufferByteSize: s.minBufferByteSize,
176 | AvgBufferByteSize: s.avgBufferByteSize,
177 | }
178 | return json.MarshalIndent(v, "", " ")
179 | }
180 |
--------------------------------------------------------------------------------
/examples/console/util.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/stellar/go/clients/horizonclient"
7 | "github.com/stellar/go/keypair"
8 | "github.com/stellar/go/txnbuild"
9 | )
10 |
11 | func createAccountWithRoot(client horizonclient.ClientInterface, networkPassphrase string, accountKey *keypair.FromAddress) error {
12 | rootKey := keypair.Root(networkPassphrase)
13 | root, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: rootKey.Address()})
14 | if err != nil {
15 | return err
16 | }
17 | tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{
18 | SourceAccount: &root,
19 | IncrementSequenceNum: true,
20 | BaseFee: txnbuild.MinBaseFee,
21 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)},
22 | Operations: []txnbuild.Operation{
23 | &txnbuild.CreateAccount{
24 | Destination: accountKey.Address(),
25 | Amount: "10000000",
26 | },
27 | },
28 | })
29 | if err != nil {
30 | return err
31 | }
32 | tx, err = tx.Sign(networkPassphrase, rootKey)
33 | if err != nil {
34 | return err
35 | }
36 | _, err = client.SubmitTransaction(tx)
37 | if err != nil {
38 | return err
39 | }
40 | return nil
41 | }
42 |
43 | func retry(maxAttempts int, f func() error) (err error) {
44 | for i := 0; i < maxAttempts; i++ {
45 | err = f()
46 | if err == nil {
47 | return
48 | }
49 | time.Sleep(100 * time.Millisecond)
50 | }
51 | return err
52 | }
53 |
--------------------------------------------------------------------------------
/examples/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
43 |
44 |
45 |
46 |
47 | Account Signers
48 |
49 |
50 |
51 |
52 |
53 |
54 | Balances
55 |
56 |
57 |
58 |
59 |
60 |
61 | Payments
62 |
63 |
Buffered Payments per Second
64 |
65 |
66 |
67 |
68 |
103 |
104 |
369 |
--------------------------------------------------------------------------------
/go.work:
--------------------------------------------------------------------------------
1 | go 1.18
2 |
3 | use (
4 | examples/console
5 | sdk
6 | )
7 |
--------------------------------------------------------------------------------
/sdk/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | # Enable these linters in addition to the default linters that golangci-lint starts with.
3 | enable:
4 | - gofmt
5 | - gofumpt
6 |
7 | linters-settings:
8 | govet:
9 | # TODO: Enable shadow checks, because stellar/go has them enabled, but they're a bit painful for us to enable now.
10 | # check-shadowing: true
11 | gofmt:
12 | simplify: true
13 |
--------------------------------------------------------------------------------
/sdk/agent/agenthttp/agenthttp.go:
--------------------------------------------------------------------------------
1 | // Package agenthttp contains a simple HTTP handler that, when requested, will
2 | // return a snapshot of an agent's snapshot at that moment.
3 | package agenthttp
4 |
5 | import (
6 | "encoding/json"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/rs/cors"
11 | "github.com/stellar/go/keypair"
12 | "github.com/stellar/starlight/sdk/agent"
13 | )
14 |
15 | // New creates a new http.Handler that returns snapshots for the given agent at
16 | // the root path. The snapshot generated is also accompanied by the agents
17 | // config that was used to create the agent at the time it was created. Secrets
18 | // keys in the config are transformed into public keys.
19 | func New(a *agent.Agent) http.Handler {
20 | m := http.NewServeMux()
21 | m.HandleFunc("/", handleSnapshot(a))
22 | return cors.Default().Handler(m)
23 | }
24 |
25 | func handleSnapshot(a *agent.Agent) func(w http.ResponseWriter, r *http.Request) {
26 | return func(w http.ResponseWriter, r *http.Request) {
27 | enc := json.NewEncoder(w)
28 | enc.SetIndent("", " ")
29 | type agentConfig struct {
30 | ObservationPeriodTime time.Duration
31 | ObservationPeriodLedgerGap uint32
32 | MaxOpenExpiry time.Duration
33 | NetworkPassphrase string
34 | ChannelAccountKey *keypair.FromAddress
35 | ChannelAccountSigner *keypair.FromAddress
36 | }
37 | c := a.Config()
38 | v := struct {
39 | Config agentConfig
40 | Snapshot agent.Snapshot
41 | }{
42 | Config: agentConfig{
43 | ObservationPeriodTime: c.ObservationPeriodTime,
44 | ObservationPeriodLedgerGap: c.ObservationPeriodLedgerGap,
45 | MaxOpenExpiry: c.MaxOpenExpiry,
46 | NetworkPassphrase: c.NetworkPassphrase,
47 | ChannelAccountKey: c.ChannelAccountKey,
48 | ChannelAccountSigner: c.ChannelAccountSigner.FromAddress(),
49 | },
50 | Snapshot: a.Snapshot(),
51 | }
52 | err := enc.Encode(v)
53 | if err != nil {
54 | panic(err)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sdk/agent/bufferedagent/agent.go:
--------------------------------------------------------------------------------
1 | // Package bufferedagent contains a rudimentary and experimental implementation
2 | // of an agent that coordinates a TCP network connection, initial handshake, and
3 | // channel opens, payments, and closes, and buffers outgoing payments,
4 | // collapsing them down to a single payment.
5 | //
6 | // The agent is intended for use in examples only at this point and is not
7 | // intended to be stable or reliable.
8 | package bufferedagent
9 |
10 | import (
11 | "errors"
12 | "fmt"
13 | "io"
14 | "math"
15 | "sync"
16 |
17 | "github.com/google/uuid"
18 | "github.com/stellar/starlight/sdk/agent"
19 | "github.com/stellar/starlight/sdk/state"
20 | )
21 |
22 | // ErrBufferFull indicates that the payment buffer has reached it's maximum size
23 | // as configured when the buffered agent was created.
24 | var ErrBufferFull = errors.New("buffer full")
25 |
26 | // Config contains the information that can be supplied to configure the Agent
27 | // at construction.
28 | type Config struct {
29 | Agent *agent.Agent
30 | AgentEvents <-chan interface{}
31 |
32 | MaxBufferSize int
33 |
34 | LogWriter io.Writer
35 |
36 | Events chan<- interface{}
37 | }
38 |
39 | // NewAgent constructs a new buffered agent with the given config.
40 | func NewAgent(c Config) *Agent {
41 | agent := &Agent{
42 | agent: c.Agent,
43 | agentEvents: c.AgentEvents,
44 |
45 | maxbufferSize: c.MaxBufferSize,
46 |
47 | logWriter: c.LogWriter,
48 |
49 | bufferReady: make(chan struct{}, 1),
50 | sendingReady: make(chan struct{}, 1),
51 | idle: make(chan struct{}),
52 |
53 | events: c.Events,
54 | }
55 | bufferReadyCloseOnce := sync.Once{}
56 | agent.bufferReadyClose = func() {
57 | bufferReadyCloseOnce.Do(func() {
58 | close(agent.bufferReady)
59 | })
60 | }
61 | agent.resetbuffer()
62 | agent.sendingReady <- struct{}{}
63 | go agent.flushLoop()
64 | return agent
65 | }
66 |
67 | // Agent coordinates a payment channel over a TCP connection, and
68 | // buffers payments by collapsing them down into single payments while it waits
69 | // for a chance to make the next payment.
70 | //
71 | // All functions of the Agent are safe to call from multiple goroutines as they
72 | // use an internal mutex.
73 | type Agent struct {
74 | maxbufferSize int
75 |
76 | logWriter io.Writer
77 |
78 | agentEvents <-chan interface{}
79 | events chan<- interface{}
80 |
81 | // mu is a lock for the mutable fields of this type. It should be locked
82 | // when reading or writing any of the mutable fields. The mutable fields are
83 | // listed below. If pushing to a chan, such as Events, it is unnecessary to
84 | // lock.
85 | mu sync.Mutex
86 |
87 | agent *agent.Agent
88 |
89 | bufferID string
90 | buffer []BufferedPayment
91 | bufferTotalAmount int64
92 | bufferReady chan struct{}
93 | bufferReadyClose func()
94 | sendingReady chan struct{}
95 | idle chan struct{}
96 | }
97 |
98 | // MaxBufferSize returns the maximum buffer size that was configured at
99 | // construction or changed with SetMaxBufferSize. The maximum buffer size is the
100 | // maximum number of payments that can be buffered while waiting for the
101 | // opportunity to include the buffered payments in an agreement.
102 | func (a *Agent) MaxBufferSize() int {
103 | a.mu.Lock()
104 | defer a.mu.Unlock()
105 | return a.maxbufferSize
106 | }
107 |
108 | // SetMaxBufferSize sets and changes the maximum buffer size.
109 | func (a *Agent) SetMaxBufferSize(maxbufferSize int) {
110 | a.mu.Lock()
111 | defer a.mu.Unlock()
112 | a.maxbufferSize = maxbufferSize
113 | }
114 |
115 | // Open opens the channel for the given asset. The open is coordinated with the
116 | // other participant. An immediate error may be indicated if the attempt to open
117 | // was immediately unsuccessful. However, more likely any error will be returned
118 | // on the events channel as the process involves the other participant.
119 | func (a *Agent) Open(asset state.Asset) error {
120 | a.mu.Lock()
121 | defer a.mu.Unlock()
122 | return a.agent.Open(asset)
123 | }
124 |
125 | // PaymentWithMemo buffers a payment which will be paid in the next agreement.
126 | // The identifier for the buffer is returned. An error may be returned
127 | // immediately if the buffer is full. Any errors relating to the payment, and
128 | // confirmation of the payment, will be returned asynchronously on the events
129 | // channel.
130 | func (a *Agent) PaymentWithMemo(paymentAmount int64, memo string) (bufferID string, err error) {
131 | a.mu.Lock()
132 | defer a.mu.Unlock()
133 | if a.maxbufferSize != 0 && len(a.buffer) == a.maxbufferSize {
134 | return "", ErrBufferFull
135 | }
136 | if paymentAmount > math.MaxInt64-a.bufferTotalAmount {
137 | return "", ErrBufferFull
138 | }
139 | a.buffer = append(a.buffer, BufferedPayment{Amount: paymentAmount, Memo: memo})
140 | a.bufferTotalAmount += paymentAmount
141 | bufferID = a.bufferID
142 | select {
143 | case a.bufferReady <- struct{}{}:
144 | default:
145 | }
146 | return
147 | }
148 |
149 | // Payment is equivalent to calling PaymentWithMemo with an empty memo.
150 | func (a *Agent) Payment(paymentAmount int64) (bufferID string, err error) {
151 | return a.PaymentWithMemo(paymentAmount, "")
152 | }
153 |
154 | // Wait waits for sending of all buffered payments to complete and the buffer to
155 | // be empty. It can be called multiple times, and it can be called in between
156 | // sends of new payments.
157 | func (a *Agent) Wait() {
158 | <-a.idle
159 | }
160 |
161 | // DeclareClose starts the close process of the channel by submitting the latest
162 | // declaration to the network, then coordinating an immediate close with the
163 | // other participant. If an immediate close can be coordinated it will
164 | // automatically occur, otherwise a participant must call Close after the
165 | // observation period has passed to close the channel.
166 | //
167 | // It is not possible to make new payments once called.
168 | func (a *Agent) DeclareClose() error {
169 | a.mu.Lock()
170 | defer a.mu.Unlock()
171 | a.bufferReadyClose()
172 | return a.agent.DeclareClose()
173 | }
174 |
175 | // Close submits the close transaction to the network. DeclareClose must have
176 | // been called by one of the participants before hand.
177 | func (a *Agent) Close() error {
178 | a.mu.Lock()
179 | defer a.mu.Unlock()
180 | a.bufferReadyClose()
181 | return a.agent.Close()
182 | }
183 |
184 | func (a *Agent) eventLoop() {
185 | defer close(a.events)
186 | defer close(a.sendingReady)
187 | defer fmt.Fprintf(a.logWriter, "event loop stopped\n")
188 | fmt.Fprintf(a.logWriter, "event loop started\n")
189 | for {
190 | ae, open := <-a.agentEvents
191 | if !open {
192 | break
193 | }
194 |
195 | // Pass all agent events up to the agent.
196 | a.events <- ae
197 |
198 | // Unpack payment received and sent events and create events for each
199 | // sub-payment that was bufferd within them.
200 | switch e := ae.(type) {
201 | case agent.PaymentReceivedEvent:
202 | memo := bufferedPaymentsMemo{}
203 | err := memo.UnmarshalBinary(e.CloseAgreement.Envelope.Details.Memo)
204 | if err != nil {
205 | a.events <- agent.ErrorEvent{Err: err}
206 | continue
207 | }
208 | a.events <- BufferedPaymentsReceivedEvent{
209 | BufferID: memo.ID,
210 | BufferByteSize: len(e.CloseAgreement.Envelope.Details.Memo),
211 | Payments: memo.Payments,
212 | }
213 | case agent.PaymentSentEvent:
214 | a.sendingReady <- struct{}{}
215 | memo := bufferedPaymentsMemo{}
216 | err := memo.UnmarshalBinary(e.CloseAgreement.Envelope.Details.Memo)
217 | if err != nil {
218 | a.events <- agent.ErrorEvent{Err: err}
219 | continue
220 | }
221 | a.events <- BufferedPaymentsSentEvent{
222 | BufferID: memo.ID,
223 | BufferByteSize: len(e.CloseAgreement.Envelope.Details.Memo),
224 | Payments: memo.Payments,
225 | }
226 | }
227 | }
228 | }
229 |
230 | func (a *Agent) flushLoop() {
231 | defer fmt.Fprintf(a.logWriter, "flush loop stopped\n")
232 | fmt.Fprintf(a.logWriter, "flush loop started\n")
233 | for {
234 | _, open := <-a.sendingReady
235 | if !open {
236 | return
237 | }
238 | select {
239 | case _, open = <-a.bufferReady:
240 | if !open {
241 | return
242 | }
243 | a.flush()
244 | default:
245 | select {
246 | case _, open = <-a.bufferReady:
247 | if !open {
248 | return
249 | }
250 | a.flush()
251 | case a.idle <- struct{}{}:
252 | a.sendingReady <- struct{}{}
253 | }
254 | }
255 | }
256 | }
257 |
258 | func (a *Agent) flush() {
259 | var bufferID string
260 | var buffer []BufferedPayment
261 | var bufferTotalAmount int64
262 |
263 | func() {
264 | a.mu.Lock()
265 | defer a.mu.Unlock()
266 |
267 | bufferID = a.bufferID
268 | buffer = a.buffer
269 | bufferTotalAmount = a.bufferTotalAmount
270 | a.resetbuffer()
271 | }()
272 |
273 | if len(buffer) == 0 {
274 | a.sendingReady <- struct{}{}
275 | return
276 | }
277 |
278 | memo := bufferedPaymentsMemo{
279 | ID: bufferID,
280 | Payments: buffer,
281 | }
282 | memoBytes, err := memo.MarshalBinary()
283 | if err != nil {
284 | a.events <- agent.ErrorEvent{Err: err}
285 | a.sendingReady <- struct{}{}
286 | return
287 | }
288 |
289 | err = a.agent.PaymentWithMemo(bufferTotalAmount, memoBytes)
290 | if err != nil {
291 | a.events <- agent.ErrorEvent{Err: err}
292 | a.sendingReady <- struct{}{}
293 | return
294 | }
295 | }
296 |
297 | func (a *Agent) resetbuffer() {
298 | a.bufferID = uuid.NewString()
299 | a.buffer = nil
300 | a.bufferTotalAmount = 0
301 | }
302 |
--------------------------------------------------------------------------------
/sdk/agent/bufferedagent/buffered_memo.go:
--------------------------------------------------------------------------------
1 | package bufferedagent
2 |
3 | import (
4 | "bytes"
5 | "encoding/gob"
6 | "fmt"
7 |
8 | "github.com/klauspost/compress/gzip"
9 | )
10 |
11 | type bufferedPaymentsMemo struct {
12 | ID string
13 | Payments []BufferedPayment
14 | }
15 |
16 | func (m *bufferedPaymentsMemo) MarshalBinary() ([]byte, error) {
17 | b := bytes.Buffer{}
18 | z, err := gzip.NewWriterLevel(&b, gzip.BestSpeed)
19 | if err != nil {
20 | panic(fmt.Errorf("creating gzip writer: %w", err))
21 | }
22 | enc := gob.NewEncoder(z)
23 | type bpm bufferedPaymentsMemo
24 | err = enc.Encode((*bpm)(m))
25 | if err != nil {
26 | return nil, fmt.Errorf("encoding buffered payments memo: %w", err)
27 | }
28 | z.Close()
29 | return b.Bytes(), nil
30 | }
31 |
32 | func (m *bufferedPaymentsMemo) UnmarshalBinary(b []byte) error {
33 | r := bytes.NewReader(b)
34 | z, err := gzip.NewReader(r)
35 | if err != nil {
36 | return fmt.Errorf("creating gzip reader: %w", err)
37 | }
38 | dec := gob.NewDecoder(z)
39 | type bpm bufferedPaymentsMemo
40 | err = dec.Decode((*bpm)(m))
41 | if err != nil {
42 | return fmt.Errorf("decoding buffered payments memo: %w", err)
43 | }
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/sdk/agent/bufferedagent/buffered_payment.go:
--------------------------------------------------------------------------------
1 | package bufferedagent
2 |
3 | // BufferedPayment contains the details of a payment that is buffered and
4 | // transmitted in the memo of an agreement on the payment channel.
5 | type BufferedPayment struct {
6 | Amount int64
7 | Memo string
8 | }
9 |
--------------------------------------------------------------------------------
/sdk/agent/bufferedagent/events.go:
--------------------------------------------------------------------------------
1 | package bufferedagent
2 |
3 | import "github.com/stellar/starlight/sdk/agent"
4 |
5 | // BufferedPaymentReceivedEvent occurs when a payment is received that was
6 | // buffered.
7 | type BufferedPaymentsReceivedEvent struct {
8 | agent.PaymentReceivedEvent
9 | BufferID string
10 | BufferByteSize int
11 | Payments []BufferedPayment
12 | }
13 |
14 | // BufferedPaymentSentEvent occurs when a payment is sent that was buffered.
15 | type BufferedPaymentsSentEvent struct {
16 | agent.PaymentSentEvent
17 | BufferID string
18 | BufferByteSize int
19 | Payments []BufferedPayment
20 | }
21 |
--------------------------------------------------------------------------------
/sdk/agent/bufferedagent/tcp.go:
--------------------------------------------------------------------------------
1 | package bufferedagent
2 |
3 | // ServeTCP listens on the given address for a single incoming connection to
4 | // start a payment channel.
5 | func (a *Agent) ServeTCP(addr string) error {
6 | go a.eventLoop()
7 | return a.agent.ServeTCP(addr)
8 | }
9 |
10 | // ConnectTCP connects to the given address for establishing a single payment
11 | // channel.
12 | func (a *Agent) ConnectTCP(addr string) error {
13 | go a.eventLoop()
14 | return a.agent.ConnectTCP(addr)
15 | }
16 |
--------------------------------------------------------------------------------
/sdk/agent/events.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "github.com/stellar/go/keypair"
5 | "github.com/stellar/starlight/sdk/state"
6 | )
7 |
8 | // ErrorEvent occurs when an error has occurred, and contains the error
9 | // occurred.
10 | type ErrorEvent struct {
11 | Err error
12 | }
13 |
14 | // ConnectedEvent occurs when the agent is connected to another participant.
15 | type ConnectedEvent struct {
16 | ChannelAccount *keypair.FromAddress
17 | Signer *keypair.FromAddress
18 | }
19 |
20 | // OpenedEvent occurs when the channel has been opened.
21 | type OpenedEvent struct {
22 | OpenAgreement state.OpenAgreement
23 | }
24 |
25 | // PaymentReceivedEvent occurs when a payment is received and the balance it
26 | // agrees to would be the resulting disbursements from the channel if closed.
27 | type PaymentReceivedEvent struct {
28 | CloseAgreement state.CloseAgreement
29 | }
30 |
31 | // PaymentSentEvent occurs when a payment is sent and the other participant has
32 | // confirmed it such that the balance the agreement agrees to would be the
33 | // resulting disbursements from the channel if closed.
34 | type PaymentSentEvent struct {
35 | CloseAgreement state.CloseAgreement
36 | }
37 |
38 | // ClosingEvent occurs when the channel is closing and no new payments should be
39 | // proposed or confirmed.
40 | type ClosingEvent struct{}
41 |
42 | // ClosingWithOutdatedStateEvent occurs when the channel is closing and no new payments should be
43 | // proposed or confirmed, and the state it is closing in is not the latest known state.
44 | type ClosingWithOutdatedStateEvent struct{}
45 |
46 | // ClosedEvent occurs when the channel is successfully closed.
47 | type ClosedEvent struct{}
48 |
--------------------------------------------------------------------------------
/sdk/agent/hash.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stellar/go/txnbuild"
7 | )
8 |
9 | func hashTx(txXDR, networkPassphrase string) (string, error) {
10 | tx, err := txnbuild.TransactionFromXDR(txXDR)
11 | if err != nil {
12 | return "", fmt.Errorf("parsing transaction xdr: %w", err)
13 | }
14 | if feeBump, ok := tx.FeeBump(); ok {
15 | hash, err := feeBump.HashHex(networkPassphrase)
16 | if err != nil {
17 | return "", fmt.Errorf("hashing fee bump tx: %w", err)
18 | }
19 | return hash, nil
20 | }
21 | if transaction, ok := tx.Transaction(); ok {
22 | hash, err := transaction.HashHex(networkPassphrase)
23 | if err != nil {
24 | return "", fmt.Errorf("hashing tx: %w", err)
25 | }
26 | return hash, nil
27 | }
28 | return "", fmt.Errorf("transaction unrecognized")
29 | }
30 |
--------------------------------------------------------------------------------
/sdk/agent/horizon/balance_collector.go:
--------------------------------------------------------------------------------
1 | package horizon
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stellar/go/amount"
7 | "github.com/stellar/go/clients/horizonclient"
8 | "github.com/stellar/go/keypair"
9 | "github.com/stellar/go/protocols/horizon"
10 | "github.com/stellar/starlight/sdk/agent"
11 | "github.com/stellar/starlight/sdk/state"
12 | )
13 |
14 | var _ agent.BalanceCollector = &BalanceCollector{}
15 |
16 | // BalanceCollector implements an agent's interface for collecting balances by
17 | // querying Horizon's accounts endpoint for the balance.
18 | type BalanceCollector struct {
19 | HorizonClient horizonclient.ClientInterface
20 | }
21 |
22 | // GetBalance queries Horizon for the balance of the given asset on the given
23 | // account.
24 | func (h *BalanceCollector) GetBalance(accountID *keypair.FromAddress, asset state.Asset) (int64, error) {
25 | var account horizon.Account
26 | account, err := h.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: accountID.Address()})
27 | if err != nil {
28 | return 0, fmt.Errorf("getting account details of %s: %w", accountID, err)
29 | }
30 | for _, b := range account.Balances {
31 | if b.Asset.Code == asset.Code() || b.Asset.Issuer == asset.Issuer() {
32 | balance, err := amount.ParseInt64(account.Balances[0].Balance)
33 | if err != nil {
34 | return 0, fmt.Errorf("parsing %s balance of %s: %w", asset, accountID, err)
35 | }
36 | return balance, nil
37 | }
38 | }
39 | return 0, nil
40 | }
41 |
--------------------------------------------------------------------------------
/sdk/agent/horizon/doc.go:
--------------------------------------------------------------------------------
1 | // Package horizon contains types that implement a variety of interfaces defined
2 | // by the sdk/agent and its sub-packages, providing the functionality of those
3 | // interfaces by querying Horizon.
4 | package horizon
5 |
--------------------------------------------------------------------------------
/sdk/agent/horizon/sequence_number_collector.go:
--------------------------------------------------------------------------------
1 | package horizon
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stellar/go/clients/horizonclient"
7 | "github.com/stellar/go/keypair"
8 | "github.com/stellar/starlight/sdk/agent"
9 | )
10 |
11 | var _ agent.SequenceNumberCollector = &SequenceNumberCollector{}
12 |
13 | // SequenceNumberCollector implements an agent's interface for collecting the
14 | // current sequence number by querying Horizon's accounts endpoint.
15 | type SequenceNumberCollector struct {
16 | HorizonClient horizonclient.ClientInterface
17 | }
18 |
19 | // GetSequenceNumber queries Horizon for the balance of the given account.
20 | func (h *SequenceNumberCollector) GetSequenceNumber(accountID *keypair.FromAddress) (int64, error) {
21 | account, err := h.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: accountID.Address()})
22 | if err != nil {
23 | return 0, fmt.Errorf("getting account details of %s: %w", accountID, err)
24 | }
25 | seqNum, err := account.GetSequenceNumber()
26 | if err != nil {
27 | return 0, fmt.Errorf("getting sequence number of account %s: %w", accountID, err)
28 | }
29 | return seqNum, nil
30 | }
31 |
--------------------------------------------------------------------------------
/sdk/agent/horizon/streamer.go:
--------------------------------------------------------------------------------
1 | package horizon
2 |
3 | import (
4 | "context"
5 | "strconv"
6 | "sync"
7 |
8 | "github.com/stellar/go/clients/horizonclient"
9 | "github.com/stellar/go/keypair"
10 | "github.com/stellar/go/protocols/horizon"
11 | "github.com/stellar/starlight/sdk/agent"
12 | )
13 |
14 | var _ agent.Streamer = &Streamer{}
15 |
16 | // Streamer implements the agent's interface for streaming transactions that
17 | // affect a set of accounts, by using the streaming endpoints of Horizon's API
18 | // to collect new transactions as they occur.
19 | type Streamer struct {
20 | HorizonClient horizonclient.ClientInterface
21 | ErrorHandler func(error)
22 | }
23 |
24 | // StreamTx streams transactions that affect the given accounts, sending each
25 | // transaction to the txs channel returned. StreamTx can be stopped by calling
26 | // the cancel function returned. If multiple accounts are given the same
27 | // transaction it may be broadcasted in duplicate if the transaction affects more
28 | // than one account being monitored. The given cursor suppors resuming a
29 | // previous stream.
30 | //
31 | // TODO: Improve StreamTx so that it only streams transactions that affect the
32 | // given accounts. At the moment, to reduce complexity and due to limitations in
33 | // Horizon, it streams all network transactions. See
34 | // https://github.com/stellar/go/issues/3874.
35 | func (h *Streamer) StreamTx(cursor string, accounts ...*keypair.FromAddress) (txs <-chan agent.StreamedTransaction, cancel func()) {
36 | // txsCh is the channel that streamed transactions will be written to.
37 | txsCh := make(chan agent.StreamedTransaction)
38 |
39 | // cancelCh will be used to signal the streamer to stop.
40 | cancelCh := make(chan struct{})
41 |
42 | // Start a streamer that will write txs and stop when
43 | // signaled to cancel.
44 | go func() {
45 | defer close(txsCh)
46 | h.streamTx(cursor, txsCh, cancelCh)
47 | }()
48 |
49 | cancelOnce := sync.Once{}
50 | cancel = func() {
51 | cancelOnce.Do(func() {
52 | close(cancelCh)
53 | })
54 | }
55 | return txsCh, cancel
56 | }
57 |
58 | func (h *Streamer) streamTx(cursor string, txs chan<- agent.StreamedTransaction, cancel <-chan struct{}) {
59 | ctx, ctxCancel := context.WithCancel(context.Background())
60 | go func() {
61 | <-cancel
62 | ctxCancel()
63 | }()
64 | for {
65 | req := horizonclient.TransactionRequest{
66 | Cursor: cursor,
67 | }
68 | err := h.HorizonClient.StreamTransactions(ctx, req, func(tx horizon.Transaction) {
69 | pagingToken := tx.PagingToken()
70 | txOrderID, err := strconv.ParseInt(pagingToken, 10, 64)
71 | if err != nil {
72 | ctxCancel()
73 | if h.ErrorHandler != nil {
74 | h.ErrorHandler(err)
75 | }
76 | return
77 | }
78 | cursor = pagingToken
79 | streamedTx := agent.StreamedTransaction{
80 | Cursor: cursor,
81 | TransactionOrderID: txOrderID,
82 | TransactionXDR: tx.EnvelopeXdr,
83 | ResultXDR: tx.ResultXdr,
84 | ResultMetaXDR: tx.ResultMetaXdr,
85 | }
86 | select {
87 | case <-cancel:
88 | ctxCancel()
89 | case txs <- streamedTx:
90 | }
91 | })
92 | if err == nil {
93 | break
94 | }
95 | if h.ErrorHandler != nil {
96 | h.ErrorHandler(err)
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/sdk/agent/horizon/streamer_test.go:
--------------------------------------------------------------------------------
1 | package horizon
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "testing"
7 |
8 | "github.com/stellar/go/clients/horizonclient"
9 | "github.com/stellar/go/keypair"
10 | "github.com/stellar/go/protocols/horizon"
11 | "github.com/stellar/starlight/sdk/agent"
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/mock"
14 | )
15 |
16 | func TestStreamer_StreamTx_longBlock(t *testing.T) {
17 | client := &horizonclient.MockClient{}
18 | h := Streamer{HorizonClient: client}
19 |
20 | accountA := keypair.MustRandom()
21 | client.On(
22 | "StreamTransactions",
23 | mock.Anything,
24 | horizonclient.TransactionRequest{},
25 | mock.Anything,
26 | ).Return(nil).Run(func(args mock.Arguments) {
27 | ctx := args[0].(context.Context)
28 | handler := args[2].(horizonclient.TransactionHandler)
29 | handler(horizon.Transaction{
30 | PT: "1",
31 | EnvelopeXdr: "a-txxdr",
32 | ResultXdr: "a-resultxdr",
33 | ResultMetaXdr: "a-resultmetaxdr",
34 | })
35 | // Simulate long block on new data from Horizon.
36 | <-ctx.Done()
37 | })
38 |
39 | t.Log("Streaming...")
40 | txsCh, cancel := h.StreamTx("", accountA.FromAddress())
41 |
42 | // Pull streamed transactions into slice.
43 | t.Log("Pulling some transactions from stream...")
44 | txs := []agent.StreamedTransaction{}
45 | txs = append(txs, <-txsCh)
46 |
47 | // Check that the streamed transactions has transactions from A and B.
48 | assert.ElementsMatch(
49 | t,
50 | []agent.StreamedTransaction{
51 | {
52 | Cursor: "1",
53 | TransactionOrderID: 1,
54 | TransactionXDR: "a-txxdr",
55 | ResultXDR: "a-resultxdr",
56 | ResultMetaXDR: "a-resultmetaxdr",
57 | },
58 | },
59 | txs,
60 | )
61 |
62 | // Cancel streaming, and check that multiple cancels are okay.
63 | t.Log("Canceling...")
64 | cancel()
65 | cancel()
66 |
67 | // Check that the transaction stream channel is closed. It may still be
68 | // producing transactions for a short period of time.
69 | open := true
70 | for open {
71 | _, open = <-txsCh
72 | t.Log("Still open, waiting for cancel...")
73 | }
74 | assert.False(t, open, "txs channel not closed but should be after cancel called")
75 | }
76 |
77 | func TestStreamer_StreamTx_manyTxs(t *testing.T) {
78 | client := &horizonclient.MockClient{}
79 | h := Streamer{HorizonClient: client}
80 |
81 | accountB := keypair.MustRandom()
82 | client.On(
83 | "StreamTransactions",
84 | mock.Anything,
85 | horizonclient.TransactionRequest{},
86 | mock.Anything,
87 | ).Return(nil).Run(func(args mock.Arguments) {
88 | ctx := args[0].(context.Context)
89 | handler := args[2].(horizonclient.TransactionHandler)
90 | // Simulate many transactions coming from Horizon.
91 | for {
92 | select {
93 | case <-ctx.Done():
94 | return
95 | default:
96 | handler(horizon.Transaction{
97 | PT: "1",
98 | EnvelopeXdr: "b-txxdr",
99 | ResultXdr: "b-resultxdr",
100 | ResultMetaXdr: "b-resultmetaxdr",
101 | })
102 | }
103 | }
104 | })
105 |
106 | t.Log("Streaming...")
107 | txsCh, cancel := h.StreamTx("", accountB.FromAddress())
108 |
109 | // Pull streamed transactions into slice.
110 | t.Log("Pulling some transactions from stream...")
111 | txs := []agent.StreamedTransaction{}
112 | txs = append(txs, <-txsCh, <-txsCh)
113 |
114 | // Check that the streamed transactions has transactions from A and B.
115 | assert.ElementsMatch(
116 | t,
117 | []agent.StreamedTransaction{
118 | {
119 | Cursor: "1",
120 | TransactionOrderID: 1,
121 | TransactionXDR: "b-txxdr",
122 | ResultXDR: "b-resultxdr",
123 | ResultMetaXDR: "b-resultmetaxdr",
124 | },
125 | {
126 | Cursor: "1",
127 | TransactionOrderID: 1,
128 | TransactionXDR: "b-txxdr",
129 | ResultXDR: "b-resultxdr",
130 | ResultMetaXDR: "b-resultmetaxdr",
131 | },
132 | },
133 | txs,
134 | )
135 |
136 | // Cancel streaming, and check that multiple cancels are okay.
137 | t.Log("Canceling...")
138 | cancel()
139 | cancel()
140 |
141 | // Check that the transaction stream channel is closed. It may still be
142 | // producing transactions for a short period of time.
143 | open := true
144 | for open {
145 | _, open = <-txsCh
146 | t.Log("Still open, waiting for cancel...")
147 | }
148 | assert.False(t, open, "txs channel not closed but should be after cancel called")
149 | }
150 |
151 | func TestHorizonStreamer_StreamTx_error(t *testing.T) {
152 | client := &horizonclient.MockClient{}
153 |
154 | errorsSeen := make(chan error, 1)
155 | h := Streamer{
156 | HorizonClient: client,
157 | ErrorHandler: func(err error) {
158 | errorsSeen <- err
159 | },
160 | }
161 |
162 | accountB := keypair.MustRandom()
163 |
164 | // Simulate an error occuring while streaming.
165 | client.On(
166 | "StreamTransactions",
167 | mock.Anything,
168 | horizonclient.TransactionRequest{},
169 | mock.Anything,
170 | ).Return(errors.New("an error")).Run(func(args mock.Arguments) {
171 | handler := args[2].(horizonclient.TransactionHandler)
172 | handler(horizon.Transaction{
173 | PT: "1",
174 | EnvelopeXdr: "a-txxdr",
175 | ResultXdr: "a-resultxdr",
176 | ResultMetaXdr: "a-resultmetaxdr",
177 | })
178 | }).Once()
179 |
180 | // Simulator no error after retrying.
181 | client.On(
182 | "StreamTransactions",
183 | mock.Anything,
184 | horizonclient.TransactionRequest{Cursor: "1"},
185 | mock.Anything,
186 | ).Return(nil).Run(func(args mock.Arguments) {
187 | ctx := args[0].(context.Context)
188 | handler := args[2].(horizonclient.TransactionHandler)
189 | handler(horizon.Transaction{
190 | PT: "2",
191 | EnvelopeXdr: "b-txxdr",
192 | ResultXdr: "b-resultxdr",
193 | ResultMetaXdr: "b-resultmetaxdr",
194 | })
195 | <-ctx.Done()
196 | }).Once()
197 |
198 | t.Log("Streaming...")
199 | txsCh, cancel := h.StreamTx("", accountB.FromAddress())
200 |
201 | // Pull streamed transactions into slice.
202 | t.Log("Pulling some transactions from stream...")
203 | txs := []agent.StreamedTransaction{}
204 | txs = append(txs, <-txsCh)
205 | assert.EqualError(t, <-errorsSeen, "an error")
206 | txs = append(txs, <-txsCh)
207 |
208 | // Check that the streamed transactions has transactions from A and B.
209 | assert.ElementsMatch(
210 | t,
211 | []agent.StreamedTransaction{
212 | {
213 | Cursor: "1",
214 | TransactionOrderID: 1,
215 | TransactionXDR: "a-txxdr",
216 | ResultXDR: "a-resultxdr",
217 | ResultMetaXDR: "a-resultmetaxdr",
218 | },
219 | {
220 | Cursor: "2",
221 | TransactionOrderID: 2,
222 | TransactionXDR: "b-txxdr",
223 | ResultXDR: "b-resultxdr",
224 | ResultMetaXDR: "b-resultmetaxdr",
225 | },
226 | },
227 | txs,
228 | )
229 |
230 | // Cancel streaming, and check that multiple cancels are okay.
231 | t.Log("Canceling...")
232 | cancel()
233 | cancel()
234 |
235 | // Check that the transaction stream channel is closed.
236 | _, open := <-txsCh
237 | assert.False(t, open, "txs channel not closed but should be after cancel called")
238 | }
239 |
--------------------------------------------------------------------------------
/sdk/agent/horizon/submitter.go:
--------------------------------------------------------------------------------
1 | package horizon
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stellar/go/clients/horizonclient"
7 | "github.com/stellar/starlight/sdk/agent/submit"
8 | )
9 |
10 | var _ submit.SubmitTxer = &Submitter{}
11 |
12 | // Submitter implements an submit's interface for submitting transaction XDRs to
13 | // the network, via Horizon's API.
14 | type Submitter struct {
15 | HorizonClient horizonclient.ClientInterface
16 | }
17 |
18 | // SubmitTx submits the given xdr as a transaction to Horizon.
19 | func (h *Submitter) SubmitTx(xdr string) error {
20 | _, err := h.HorizonClient.SubmitTransactionXDR(xdr)
21 | if err != nil {
22 | return fmt.Errorf("submitting tx %s: %w", xdr, buildErr(err))
23 | }
24 | return nil
25 | }
26 |
27 | func buildErr(err error) error {
28 | if hErr := horizonclient.GetError(err); hErr != nil {
29 | resultString, rErr := hErr.ResultString()
30 | if rErr != nil {
31 | resultString = ""
32 | }
33 | return fmt.Errorf("%w (%v)", err, resultString)
34 | }
35 | return err
36 | }
37 |
--------------------------------------------------------------------------------
/sdk/agent/ingest.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/stellar/starlight/sdk/state"
8 | )
9 |
10 | var ingestingFinished = errors.New("ingesting finished")
11 |
12 | func (a *Agent) ingest() error {
13 | tx, ok := <-a.streamerTransactions
14 | if !ok {
15 | return ingestingFinished
16 | }
17 |
18 | a.mu.Lock()
19 | defer a.mu.Unlock()
20 |
21 | txHash, err := hashTx(tx.TransactionXDR, a.networkPassphrase)
22 | if err != nil {
23 | err = fmt.Errorf("ingesting tx (cursor=%s): hashing tx: %w", tx.Cursor, err)
24 | a.events <- ErrorEvent{Err: err}
25 | return err
26 | }
27 | fmt.Fprintf(a.logWriter, "ingesting cursor: %s tx: %s\n", tx.Cursor, txHash)
28 |
29 | stateBefore, err := a.channel.State()
30 | if err != nil {
31 | err = fmt.Errorf("ingesting tx (cursor=%s hash=%s): getting channel state before: %w", tx.Cursor, txHash, err)
32 | a.events <- ErrorEvent{Err: err}
33 | return err
34 | }
35 | fmt.Fprintf(a.logWriter, "state before: %v\n", stateBefore)
36 |
37 | defer a.takeSnapshot()
38 |
39 | err = a.channel.IngestTx(tx.TransactionOrderID, tx.TransactionXDR, tx.ResultXDR, tx.ResultMetaXDR)
40 | if err != nil {
41 | err = fmt.Errorf("ingesting tx (cursor=%s hash=%s): ingesting xdr: %w", tx.Cursor, txHash, err)
42 | a.events <- ErrorEvent{Err: err}
43 | return err
44 | }
45 |
46 | stateAfter, err := a.channel.State()
47 | if err != nil {
48 | err = fmt.Errorf("ingesting tx (cursor=%s hash=%s): getting channel state after: %w", tx.Cursor, txHash, err)
49 | a.events <- ErrorEvent{Err: err}
50 | return err
51 | }
52 | fmt.Fprintf(a.logWriter, "state after: %v\n", stateAfter)
53 |
54 | if a.events != nil {
55 | if stateAfter != stateBefore {
56 | fmt.Fprintf(a.logWriter, "writing event: %v\n", stateAfter)
57 | switch stateAfter {
58 | case state.StateOpen:
59 | a.events <- OpenedEvent{a.channel.OpenAgreement()}
60 | case state.StateClosing:
61 | a.events <- ClosingEvent{}
62 | case state.StateClosingWithOutdatedState:
63 | a.events <- ClosingWithOutdatedStateEvent{}
64 | case state.StateClosed:
65 | a.streamerCancel()
66 | a.events <- ClosedEvent{}
67 | }
68 | }
69 | }
70 |
71 | return nil
72 | }
73 |
74 | func (a *Agent) ingestLoop() {
75 | for {
76 | err := a.ingest()
77 | if err != nil {
78 | fmt.Fprintf(a.logWriter, "error ingesting: %v\n", err)
79 | }
80 | if errors.Is(err, ingestingFinished) {
81 | break
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/sdk/agent/msg/msg.go:
--------------------------------------------------------------------------------
1 | // Package msg contains simple types to assist with transmitting and
2 | // communicating across a network about a payment channel between two
3 | // participants. It is rather rudimentary and intended for use in examples.
4 | // There are no stability or compatibility guarantees for the messages or
5 | // encoding format used.
6 | package msg
7 |
8 | import (
9 | "encoding/gob"
10 | "io"
11 |
12 | "github.com/stellar/go/keypair"
13 | "github.com/stellar/starlight/sdk/state"
14 | )
15 |
16 | // Type is the message type, used to indicate which message is contained inside
17 | // a Message.
18 | type Type int
19 |
20 | const (
21 | TypeHello Type = 10
22 | TypeOpenRequest Type = 20
23 | TypeOpenResponse Type = 21
24 | TypePaymentRequest Type = 30
25 | TypePaymentResponse Type = 31
26 | TypeCloseRequest Type = 40
27 | TypeCloseResponse Type = 41
28 | )
29 |
30 | // Message is a message that can be transmitted to support two participants in a
31 | // payment channel communicating by signaling who they are with a hello, opening
32 | // the channel, making payments, and closing the channel.
33 | type Message struct {
34 | Type Type
35 |
36 | Hello *Hello
37 |
38 | OpenRequest *state.OpenEnvelope
39 | OpenResponse *state.OpenSignatures
40 |
41 | PaymentRequest *state.CloseEnvelope
42 | PaymentResponse *state.CloseSignatures
43 |
44 | CloseRequest *state.CloseEnvelope
45 | CloseResponse *state.CloseSignatures
46 | }
47 |
48 | // Hello can be used to signal to another participant a minimal amount of
49 | // information the other participant needs to know about them.
50 | type Hello struct {
51 | ChannelAccount keypair.FromAddress
52 | Signer keypair.FromAddress
53 | }
54 |
55 | // Encoder is an encoder that can be used to encode messages.
56 | // It is currently set as the encoding/gob.Encoder, but may be changed to
57 | // another type at anytime to facilitate testing or to improve performance.
58 | type Encoder = gob.Encoder
59 |
60 | func NewEncoder(w io.Writer) *Encoder {
61 | return gob.NewEncoder(w)
62 | }
63 |
64 | type Decoder = gob.Decoder
65 |
66 | func NewDecoder(r io.Reader) *Decoder {
67 | return gob.NewDecoder(r)
68 | }
69 |
--------------------------------------------------------------------------------
/sdk/agent/submit/submitter.go:
--------------------------------------------------------------------------------
1 | // Package submit contains a type and logic for submitting transactions that need to be fee bumped.
2 | //
3 | // The Submitter type can be used to wrap any submission logic, and it will
4 | // check if the transactions fee is below a threshold that indicates it needs to
5 | // be fee bumped, and if so, it will wrap it in a fee bump transaction before
6 | // submission.
7 | //
8 | // This package is intended for use in example payment channel implementations.
9 | package submit
10 |
11 | import (
12 | "fmt"
13 |
14 | "github.com/stellar/go/clients/horizonclient"
15 | "github.com/stellar/go/keypair"
16 | "github.com/stellar/go/txnbuild"
17 | )
18 |
19 | // SubmitTxer is an implementation of submitting transaction XDR to the network.
20 | type SubmitTxer interface {
21 | SubmitTx(xdr string) error
22 | }
23 |
24 | // Submitter submits transactions to the network via Horizon. If a transaction
25 | // has a base fee below the submitters base fee, the transaction is wrapped in a
26 | // fee bump transaction. This means fee-less transactions are wrapped in fee
27 | // bump transaction.
28 | //
29 | // The BaseFee is the base fee that will be used for any submission where the
30 | // transaction has a lower base fee.
31 | type Submitter struct {
32 | SubmitTxer SubmitTxer
33 | NetworkPassphrase string
34 | BaseFee int64
35 | FeeAccount *keypair.FromAddress
36 | FeeAccountSigners []*keypair.Full
37 | }
38 |
39 | // SubmitTx submits the transaction. If the transaction has a base fee that is
40 | // lower than the submitters base fee it is wrapped in a fee bump transaction
41 | // with the Submitter's FeeAccount as the fee account.
42 | func (s *Submitter) SubmitTx(tx *txnbuild.Transaction) error {
43 | if tx.BaseFee() < s.BaseFee {
44 | return s.submitTxWithFeeBump(tx)
45 | }
46 | return s.submitTx(tx)
47 | }
48 |
49 | func (s *Submitter) submitTx(tx *txnbuild.Transaction) error {
50 | txeBase64, err := tx.Base64()
51 | if err != nil {
52 | return fmt.Errorf("encoding tx as base64: %w", err)
53 | }
54 | err = s.SubmitTxer.SubmitTx(txeBase64)
55 | if err != nil {
56 | return fmt.Errorf("submitting tx: %w", buildErr(err))
57 | }
58 | return nil
59 | }
60 |
61 | func (s *Submitter) submitTxWithFeeBump(tx *txnbuild.Transaction) error {
62 | feeBumpTx, err := txnbuild.NewFeeBumpTransaction(txnbuild.FeeBumpTransactionParams{
63 | Inner: tx,
64 | BaseFee: s.BaseFee,
65 | FeeAccount: s.FeeAccount.Address(),
66 | })
67 | if err != nil {
68 | return fmt.Errorf("building fee bump tx: %w", err)
69 | }
70 | feeBumpTx, err = feeBumpTx.Sign(s.NetworkPassphrase, s.FeeAccountSigners...)
71 | if err != nil {
72 | return fmt.Errorf("signing fee bump tx: %w", err)
73 | }
74 | txeBase64, err := feeBumpTx.Base64()
75 | if err != nil {
76 | return fmt.Errorf("encoding fee bump tx as base64: %w", err)
77 | }
78 | err = s.SubmitTxer.SubmitTx(txeBase64)
79 | if err != nil {
80 | return fmt.Errorf("submitting fee bump tx: %w", buildErr(err))
81 | }
82 |
83 | return nil
84 | }
85 |
86 | func buildErr(err error) error {
87 | if hErr := horizonclient.GetError(err); hErr != nil {
88 | resultString, rErr := hErr.ResultString()
89 | if rErr != nil {
90 | resultString = ""
91 | }
92 | return fmt.Errorf("%w (%v)", err, resultString)
93 | }
94 | return err
95 | }
96 |
--------------------------------------------------------------------------------
/sdk/agent/tcp.go:
--------------------------------------------------------------------------------
1 | package agent
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | )
7 |
8 | // ServeTCP listens on the given address for a single incoming connection to
9 | // start a payment channel.
10 | func (a *Agent) ServeTCP(addr string) error {
11 | if a.conn != nil {
12 | return fmt.Errorf("already connected")
13 | }
14 | ln, err := net.Listen("tcp", addr)
15 | if err != nil {
16 | return fmt.Errorf("listening on %s: %w", addr, err)
17 | }
18 | conn, err := ln.Accept()
19 | if err != nil {
20 | return fmt.Errorf("accepting incoming connection: %w", err)
21 | }
22 | fmt.Fprintf(a.logWriter, "accepted connection from %v\n", conn.RemoteAddr())
23 | a.conn = conn
24 | err = a.hello()
25 | if err != nil {
26 | return fmt.Errorf("sending hello: %w", err)
27 | }
28 | go a.receiveLoop()
29 | return nil
30 | }
31 |
32 | // ConnectTCP connects to the given address for establishing a single payment
33 | // channel.
34 | func (a *Agent) ConnectTCP(addr string) error {
35 | if a.conn != nil {
36 | return fmt.Errorf("already connected")
37 | }
38 | var err error
39 | conn, err := net.Dial("tcp", addr)
40 | if err != nil {
41 | return fmt.Errorf("connecting to %s: %w", addr, err)
42 | }
43 | fmt.Fprintf(a.logWriter, "connected to %v\n", conn.RemoteAddr())
44 | a.conn = conn
45 | err = a.hello()
46 | if err != nil {
47 | return fmt.Errorf("sending hello: %w", err)
48 | }
49 | go a.receiveLoop()
50 | return nil
51 | }
52 |
--------------------------------------------------------------------------------
/sdk/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/stellar/starlight/sdk
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/google/gofuzz v1.2.0
7 | github.com/google/uuid v1.2.0
8 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd
9 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00
10 | github.com/stellar/go v0.0.0-20220419042134-9f968df09eda
11 | github.com/stretchr/objx v0.3.0 // indirect
12 | github.com/stretchr/testify v1.7.0
13 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
14 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
15 | )
16 |
17 | require (
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/go-chi/chi v4.0.3+incompatible // indirect
20 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 // indirect
21 | github.com/gorilla/schema v1.1.0 // indirect
22 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc // indirect
23 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect
24 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
25 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
26 | github.com/pkg/errors v0.9.1 // indirect
27 | github.com/pmezard/go-difflib v1.0.0 // indirect
28 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 // indirect
29 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect
30 | github.com/sirupsen/logrus v1.4.1 // indirect
31 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee // indirect
32 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
33 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
34 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
35 | gopkg.in/yaml.v2 v2.3.0 // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/sdk/go.sum:
--------------------------------------------------------------------------------
1 | github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU=
6 | github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4=
7 | github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY=
8 | github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
9 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601 h1:jxTbmDuqQUTI6MscgbqB39vtxGfr2fi61nYIcFQUnlE=
10 | github.com/go-errors/errors v0.0.0-20150906023321-a41850380601/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
11 | github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU=
12 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
13 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
14 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
15 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
16 | github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
17 | github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
18 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
19 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
20 | github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 h1:Aw95BEvxJ3K6o9GGv5ppCd1P8hkeIeEJ30FO+OhOJpM=
21 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd h1:vQ0EEfHpdFUtNRj1ri25MUq5jb3Vma+kKhLyjeUTVow=
22 | github.com/klauspost/compress v0.0.0-20161106143436-e3b7981a12dd/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
23 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc h1:WW8B7p7QBnFlqRVv/k6ro/S8Z7tCnYjJHcQNScx9YVs=
24 | github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
25 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 h1:KAZ1BW2TCmT6PRihDPpocIy1QTtsAsrx6TneU/4+CMg=
26 | github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
27 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
28 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
29 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
30 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
31 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
32 | github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM=
33 | github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ=
34 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
35 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
36 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
37 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
40 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00 h1:8DPul/X0IT/1TNMIxoKLwdemEOBBHDC/K4EB16Cw5WE=
41 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
42 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521 h1:3hxavr+IHMsQBrYUPQM5v0CgENFktkkbg1sfpgM3h20=
43 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
44 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc=
45 | github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y=
46 | github.com/sergi/go-diff v0.0.0-20161205080420-83532ca1c1ca h1:oR/RycYTFTVXzND5r4FdsvbnBn0HJXSVeNAnwaTXRwk=
47 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
48 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
49 | github.com/stellar/go v0.0.0-20220406183204-45b6f52202f3 h1:PLVF895y1tGeDj1L2Wzocl6Vn60zYCSAKtuATczbLYc=
50 | github.com/stellar/go v0.0.0-20220419042134-9f968df09eda h1:wIBsNGn+W8ZjFFY5ScqU2p1IDsCOSp9eUtC0+FYqikE=
51 | github.com/stellar/go v0.0.0-20220419042134-9f968df09eda/go.mod h1:XDw7zGAJCmEuZVmcr7PvLioASsqQRN80dL+OkOxV50A=
52 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee h1:fbVs0xmXpBvVS4GBeiRmAE3Le70ofAqFMch1GTiq/e8=
53 | github.com/stellar/go-xdr v0.0.0-20211103144802-8017fc4bdfee/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps=
54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
55 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
56 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
57 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
60 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
61 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
62 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
63 | github.com/valyala/fasthttp v0.0.0-20170109085056-0a7f0a797cd6 h1:s0IDmR1jFyWvOK7jVIuAsmHQaGkXUuTas8NXFUOwuAI=
64 | github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc=
65 | github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 h1:KM4T3G70MiR+JtqplcYkNVoNz7pDwYaBxWBXQK804So=
66 | github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c h1:XZWnr3bsDQWAZg4Ne+cPoXRPILrNlPNQfxBuwLl43is=
67 | github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce h1:cVSRGH8cOveJNwFEEZLXtB+XMnRqKLjUP6V/ZFYQCXI=
68 | github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb h1:06WAhQa+mYv7BiOk13B/ywyTlkoE/S7uu6TBKU6FHnE=
69 | github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
70 | github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce h1:888GrqRxabUce7lj4OaoShPxodm3kXOMpSa85wdYzfY=
71 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
72 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
73 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
74 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
75 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
76 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
77 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
80 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
81 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
82 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
83 | gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw=
84 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
85 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
86 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
87 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
88 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
89 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90 |
--------------------------------------------------------------------------------
/sdk/state/asset.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/stellar/go/txnbuild"
8 | "github.com/stellar/go/xdr"
9 | )
10 |
11 | // Asset is a Stellar asset.
12 | type Asset string
13 |
14 | const NativeAsset = Asset("native")
15 |
16 | // IsNative returns true if the asset is the native asset of the stellar
17 | // network.
18 | func (a Asset) IsNative() bool {
19 | return a.Asset().IsNative()
20 | }
21 |
22 | // Code returns the asset code of credit assets.
23 | func (a Asset) Code() string {
24 | return a.Asset().GetCode()
25 | }
26 |
27 | // Issuer returns the issuer of credit assets.
28 | func (a Asset) Issuer() string {
29 | return a.Asset().GetIssuer()
30 | }
31 |
32 | // Asset returns an asset from the stellar/go/txnbuild package with the
33 | // same asset code and issuer, or a native asset if a native asset.
34 | func (a Asset) Asset() txnbuild.Asset {
35 | parts := strings.SplitN(string(a), ":", 2)
36 | if len(parts) == 1 {
37 | return txnbuild.NativeAsset{}
38 | }
39 | return txnbuild.CreditAsset{
40 | Code: parts[0],
41 | Issuer: parts[1],
42 | }
43 | }
44 |
45 | // StringCanonical returns a string friendly representation of the asset in
46 | // canonical form.
47 | func (a Asset) StringCanonical() string {
48 | if a.IsNative() {
49 | return xdr.AssetTypeToString[xdr.AssetTypeAssetTypeNative]
50 | }
51 | return fmt.Sprintf("%s:%s", a.Code(), a.Issuer())
52 | }
53 |
54 | // EqualTrustLineAsset returns true if the canonical strings of this asset and a
55 | // given trustline asset are equal, else false.
56 | func (a Asset) EqualTrustLineAsset(ta xdr.TrustLineAsset) bool {
57 | switch ta.Type {
58 | case xdr.AssetTypeAssetTypeNative, xdr.AssetTypeAssetTypeCreditAlphanum4, xdr.AssetTypeAssetTypeCreditAlphanum12:
59 | return ta.ToAsset().StringCanonical() == a.StringCanonical()
60 | }
61 | return false
62 | }
63 |
--------------------------------------------------------------------------------
/sdk/state/asset_test.go:
--------------------------------------------------------------------------------
1 | package state_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stellar/go/txnbuild"
8 | "github.com/stellar/starlight/sdk/state"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestAsset(t *testing.T) {
13 | testCases := []struct {
14 | Asset state.Asset
15 | WantTxnbuildAsset txnbuild.Asset
16 | WantIsNative bool
17 | WantCode string
18 | WantIssuer string
19 | }{
20 | {state.Asset(""), txnbuild.NativeAsset{}, true, "", ""},
21 | {state.Asset("native"), txnbuild.NativeAsset{}, true, "", ""},
22 | {state.NativeAsset, txnbuild.NativeAsset{}, true, "", ""},
23 | {state.Asset(":"), txnbuild.CreditAsset{}, false, "", ""},
24 | {state.Asset("ABCD:GABCD"), txnbuild.CreditAsset{Code: "ABCD", Issuer: "GABCD"}, false, "ABCD", "GABCD"},
25 | {state.Asset("ABCD:GABCD:AB"), txnbuild.CreditAsset{Code: "ABCD", Issuer: "GABCD:AB"}, false, "ABCD", "GABCD:AB"},
26 | }
27 | for _, tc := range testCases {
28 | t.Run(fmt.Sprint(tc.Asset), func(t *testing.T) {
29 | assert.Equal(t, tc.WantTxnbuildAsset, tc.Asset.Asset())
30 | assert.Equal(t, tc.WantIsNative, tc.Asset.IsNative())
31 | assert.Equal(t, tc.WantCode, tc.Asset.Code())
32 | assert.Equal(t, tc.WantIssuer, tc.Asset.Issuer())
33 | })
34 | }
35 | }
36 |
37 | func TestStringCanonical(t *testing.T) {
38 | testCases := []struct {
39 | Asset state.Asset
40 | WantStringCanonical string
41 | }{
42 | {state.Asset(""), "native"},
43 | {state.Asset("native"), "native"},
44 | {state.NativeAsset, "native"},
45 | {state.Asset(":"), ":"},
46 | {state.Asset("ABCD:GABCD"), "ABCD:GABCD"},
47 | {state.Asset("ABCD:GABCD:AB"), "ABCD:GABCD:AB"},
48 | }
49 | for _, tc := range testCases {
50 | t.Run(fmt.Sprint(tc.Asset), func(t *testing.T) {
51 | assert.Equal(t, tc.WantStringCanonical, tc.Asset.StringCanonical())
52 | })
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/sdk/state/close.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stellar/go/txnbuild"
7 | "github.com/stellar/starlight/sdk/txbuild"
8 | )
9 |
10 | // closeTxs builds the transactions that can be submitted to close the channel
11 | // with the state defined in the CloseAgreementDetails, and that was opened with
12 | // the given OpenAgreementDetails. If the channel has previous build these close
13 | // transactions and still has them stored internally then it will return those
14 | // previously built transactions, otherwise the transactions will be built from
15 | // scratch.
16 | func (c *Channel) closeTxs(oad OpenDetails, d CloseDetails) (txs CloseTransactions, err error) {
17 | if c.openAgreement.Envelope.Details.Equal(oad) {
18 | if c.latestAuthorizedCloseAgreement.Envelope.Details.Equal(d) {
19 | return c.latestAuthorizedCloseAgreement.Transactions, nil
20 | }
21 | if c.latestUnauthorizedCloseAgreement.Envelope.Details.Equal(d) {
22 | return c.latestUnauthorizedCloseAgreement.Transactions, nil
23 | }
24 | }
25 | txClose, err := txbuild.Close(txbuild.CloseParams{
26 | ObservationPeriodTime: d.ObservationPeriodTime,
27 | ObservationPeriodLedgerGap: d.ObservationPeriodLedgerGap,
28 | InitiatorSigner: c.initiatorSigner(),
29 | ResponderSigner: c.responderSigner(),
30 | InitiatorChannelAccount: c.initiatorChannelAccount().Address,
31 | ResponderChannelAccount: c.responderChannelAccount().Address,
32 | StartSequence: oad.StartingSequence,
33 | IterationNumber: d.IterationNumber,
34 | AmountToInitiator: amountToInitiator(d.Balance),
35 | AmountToResponder: amountToResponder(d.Balance),
36 | Asset: oad.Asset.Asset(),
37 | })
38 | if err != nil {
39 | return CloseTransactions{}, err
40 | }
41 | txCloseHash, err := txClose.Hash(c.networkPassphrase)
42 | if err != nil {
43 | return CloseTransactions{}, err
44 | }
45 | txDecl, err := txbuild.Declaration(txbuild.DeclarationParams{
46 | InitiatorChannelAccount: c.initiatorChannelAccount().Address,
47 | StartSequence: oad.StartingSequence,
48 | IterationNumber: d.IterationNumber,
49 | IterationNumberExecuted: 0,
50 | ConfirmingSigner: d.ConfirmingSigner,
51 | CloseTxHash: txCloseHash,
52 | })
53 | if err != nil {
54 | return CloseTransactions{}, err
55 | }
56 | txDeclHash, err := txDecl.Hash(c.networkPassphrase)
57 | if err != nil {
58 | return CloseTransactions{}, err
59 | }
60 | txs = CloseTransactions{
61 | DeclarationHash: txDeclHash,
62 | Declaration: txDecl,
63 | CloseHash: txCloseHash,
64 | Close: txClose,
65 | }
66 | return txs, nil
67 | }
68 |
69 | // CloseTxs builds the declaration and close transactions used for closing the
70 | // channel using the latest close agreement. The transactions are signed and
71 | // ready to submit.
72 | func (c *Channel) CloseTxs() (declTx *txnbuild.Transaction, closeTx *txnbuild.Transaction, err error) {
73 | cae := c.latestAuthorizedCloseAgreement
74 | txs := cae.SignedTransactions()
75 | return txs.Declaration, txs.Close, nil
76 | }
77 |
78 | // ProposeClose proposes that the latest authorized close agreement be submitted
79 | // without waiting the observation period. This should be used when participants
80 | // are in agreement on the final close state, but would like to submit earlier
81 | // than the original observation time.
82 | func (c *Channel) ProposeClose() (CloseAgreement, error) {
83 | // If an unfinished unauthorized agreement exists, error.
84 | if !c.latestUnauthorizedCloseAgreement.Envelope.Empty() {
85 | return CloseAgreement{}, fmt.Errorf("cannot propose coordinated close while an unfinished payment exists")
86 | }
87 |
88 | // If the channel is not open yet, error.
89 | if c.latestAuthorizedCloseAgreement.Envelope.Empty() || !c.openExecutedAndValidated {
90 | return CloseAgreement{}, fmt.Errorf("cannot propose a coordinated close before channel is opened")
91 | }
92 |
93 | d := c.latestAuthorizedCloseAgreement.Envelope.Details
94 | d.ObservationPeriodTime = 0
95 | d.ObservationPeriodLedgerGap = 0
96 | d.ProposingSigner = c.localSigner.FromAddress()
97 | d.ConfirmingSigner = c.remoteSigner
98 |
99 | txs, err := c.closeTxs(c.openAgreement.Envelope.Details, d)
100 | if err != nil {
101 | return CloseAgreement{}, fmt.Errorf("making declaration and close transactions: %w", err)
102 | }
103 | sigs, err := signCloseAgreementTxs(txs, c.localSigner)
104 | if err != nil {
105 | return CloseAgreement{}, fmt.Errorf("signing open agreement with local: %w", err)
106 | }
107 |
108 | // Store the close agreement while participants iterate on signatures.
109 | c.latestUnauthorizedCloseAgreement = CloseAgreement{
110 | Envelope: CloseEnvelope{
111 | Details: d,
112 | ProposerSignatures: sigs,
113 | },
114 | Transactions: txs,
115 | }
116 | return c.latestUnauthorizedCloseAgreement, nil
117 | }
118 |
119 | func (c *Channel) validateClose(ca CloseEnvelope) error {
120 | // If the channel is not open yet, error.
121 | if c.latestAuthorizedCloseAgreement.Envelope.Empty() || !c.openExecutedAndValidated {
122 | return fmt.Errorf("cannot confirm a coordinated close before channel is opened")
123 | }
124 | if ca.Details.IterationNumber != c.latestAuthorizedCloseAgreement.Envelope.Details.IterationNumber {
125 | return fmt.Errorf("close agreement iteration number does not match saved latest authorized close agreement")
126 | }
127 | if ca.Details.Balance != c.latestAuthorizedCloseAgreement.Envelope.Details.Balance {
128 | return fmt.Errorf("close agreement balance does not match saved latest authorized close agreement")
129 | }
130 | if ca.Details.ObservationPeriodTime != 0 {
131 | return fmt.Errorf("close agreement observation period time is not zero")
132 | }
133 | if ca.Details.ObservationPeriodLedgerGap != 0 {
134 | return fmt.Errorf("close agreement observation period ledger gap is not zero")
135 | }
136 | if !ca.Details.ConfirmingSigner.Equal(c.localSigner.FromAddress()) && !ca.Details.ConfirmingSigner.Equal(c.remoteSigner) {
137 | return fmt.Errorf("close agreement confirmer does not match a local or remote signer, got: %s", ca.Details.ConfirmingSigner.Address())
138 | }
139 | return nil
140 | }
141 |
142 | // ConfirmClose agrees to a close agreement to be submitted without waiting the
143 | // observation period. The agreement will always be accepted if it is identical
144 | // to the latest authorized close agreement, and it is signed by the participant
145 | // proposing the close.
146 | func (c *Channel) ConfirmClose(ce CloseEnvelope) (closeAgreement CloseAgreement, err error) {
147 | err = c.validateClose(ce)
148 | if err != nil {
149 | return CloseAgreement{}, fmt.Errorf("validating close agreement: %w", err)
150 | }
151 |
152 | txs, err := c.closeTxs(c.openAgreement.Envelope.Details, ce.Details)
153 | if err != nil {
154 | return CloseAgreement{}, fmt.Errorf("making close transactions: %w", err)
155 | }
156 |
157 | remoteSigs := ce.SignaturesFor(c.remoteSigner)
158 | if remoteSigs == nil {
159 | return CloseAgreement{}, fmt.Errorf("remote is not a signer")
160 | }
161 |
162 | localSigs := ce.SignaturesFor(c.localSigner.FromAddress())
163 | if localSigs == nil {
164 | return CloseAgreement{}, fmt.Errorf("local is not a signer")
165 | }
166 |
167 | // If remote has not signed the txs or signatures is invalid, or the local
168 | // signatures if present are invalid, error as is invalid.
169 | verifyInputs := []signatureVerificationInput{
170 | {TransactionHash: txs.DeclarationHash, Signature: remoteSigs.Declaration, Signer: c.remoteSigner},
171 | {TransactionHash: txs.CloseHash, Signature: remoteSigs.Close, Signer: c.remoteSigner},
172 | }
173 | if !localSigs.Empty() {
174 | verifyInputs = append(verifyInputs, []signatureVerificationInput{
175 | {TransactionHash: txs.DeclarationHash, Signature: localSigs.Declaration, Signer: c.localSigner.FromAddress()},
176 | {TransactionHash: txs.CloseHash, Signature: localSigs.Close, Signer: c.localSigner.FromAddress()},
177 | }...)
178 | }
179 | err = verifySignatures(verifyInputs)
180 | if err != nil {
181 | return CloseAgreement{}, fmt.Errorf("invalid signature: %w", err)
182 | }
183 |
184 | // If local has not signed close, check that the payment is not to the proposer, then sign.
185 | if localSigs.Empty() {
186 | // If the local is not the confirmer, do not sign, because being the
187 | // proposer they should have signed earlier.
188 | if !ce.Details.ConfirmingSigner.Equal(c.localSigner.FromAddress()) {
189 | return CloseAgreement{}, fmt.Errorf("not signed by local")
190 | }
191 | ce.ConfirmerSignatures, err = signCloseAgreementTxs(txs, c.localSigner)
192 | if err != nil {
193 | return CloseAgreement{}, fmt.Errorf("local signing: %w", err)
194 | }
195 | }
196 |
197 | // The new close agreement is valid and authorized, store and promote it.
198 | c.latestAuthorizedCloseAgreement = CloseAgreement{
199 | Envelope: ce,
200 | Transactions: txs,
201 | }
202 | c.latestUnauthorizedCloseAgreement = CloseAgreement{}
203 | return c.latestAuthorizedCloseAgreement, nil
204 | }
205 |
--------------------------------------------------------------------------------
/sdk/state/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package state contains a state machine, contained in the Channel type, for
3 | managing a Starlight payment channel.
4 |
5 | The Channel type once constructed contains functions for three categories of
6 | operations:
7 | - Open: Opening the payment channel.
8 | - Payment: Making a payment from either participant to the other participant.
9 | - Close: Coordinating an immediate close the payment channel.
10 |
11 | The Channel type also provides functions for ingesting data from the network
12 | into the state machine. This is necessary to progress the state of the channel
13 | through states that are based on network activity, such as open and close.
14 |
15 | The Open, Payment, and Close operations are broken up into three steps:
16 | - Propose: Called by the payer to create the agreement.
17 | - Confirm: Called by the payee to confirm the agreement.
18 | - Finalize*: Called by the payer to finalize the agreement with the payees
19 | signatures.
20 |
21 | +-----------+ +-----------+
22 | | Payer | | Payee |
23 | +-----+-----+ +-----+-----+
24 | | |
25 | Propose |
26 | +----------------->+
27 | | Confirm
28 | +<-----------------+
29 | Finalize* |
30 | | |
31 |
32 | * Note that the Open and Close processes do not have a Finalize operation, and the
33 | Confirm is used in its place at this time. A Finalize operation is likely to be
34 | added in the future.
35 |
36 | None of the primitives in this package are threadsafe and synchronization
37 | must be provided by the caller if the package is used in a concurrent
38 | context.
39 | */
40 | package state
41 |
--------------------------------------------------------------------------------
/sdk/state/hash.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | )
7 |
8 | type TransactionHash [32]byte
9 |
10 | func (h TransactionHash) String() string {
11 | return hex.EncodeToString(h[:])
12 | }
13 |
14 | func (h TransactionHash) MarshalText() ([]byte, error) {
15 | text := [len(h) * 2]byte{}
16 | n := hex.Encode(text[:], h[:])
17 | if n != len(text) {
18 | return nil, hex.ErrLength
19 | }
20 | return text[:], nil
21 | }
22 |
23 | func (h *TransactionHash) UnmarshalText(text []byte) error {
24 | if len(text) != len(h)*2 {
25 | return fmt.Errorf("unmarshaling transaction hash: input length %d expected %d", len(text), len(h)*2)
26 | }
27 | n, err := hex.Decode(h[:], text)
28 | if err != nil {
29 | return fmt.Errorf("unmarshaling transaction hash: %w", err)
30 | }
31 | if n != len(h) {
32 | return fmt.Errorf("unmarshaling transaction hash: decoded length %d expected %d", n, len(h))
33 | }
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/sdk/state/hash_test.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestHash_String(t *testing.T) {
11 | h := TransactionHash{}
12 | assert.Equal(
13 | t,
14 | "0000000000000000000000000000000000000000000000000000000000000000",
15 | h.String(),
16 | )
17 | h = TransactionHash{0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01}
18 | assert.Equal(
19 | t,
20 | "0123456789012345678901234567890101234567890123456789012345678901",
21 | h.String(),
22 | )
23 | }
24 |
25 | func TestHash_MarshalText(t *testing.T) {
26 | h := TransactionHash{}
27 | b, err := h.MarshalText()
28 | require.NoError(t, err)
29 | wantB := []byte("0000000000000000000000000000000000000000000000000000000000000000")
30 | assert.Equal(t, wantB, b)
31 |
32 | h = TransactionHash{0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01}
33 | b, err = h.MarshalText()
34 | require.NoError(t, err)
35 | wantB = []byte("0123456789012345678901234567890101234567890123456789012345678901")
36 | assert.Equal(t, wantB, b)
37 | }
38 |
39 | func TestHash_UnmarshalText(t *testing.T) {
40 | // Zero.
41 | s := "0000000000000000000000000000000000000000000000000000000000000000"
42 | h := TransactionHash{}
43 | err := h.UnmarshalText([]byte(s))
44 | require.NoError(t, err)
45 | wantH := TransactionHash{}
46 | assert.Equal(t, wantH, h)
47 |
48 | // Valid.
49 | s = "0123456789012345678901234567890101234567890123456789012345678901"
50 | h = TransactionHash{}
51 | err = h.UnmarshalText([]byte(s))
52 | require.NoError(t, err)
53 | wantH = TransactionHash{0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01, 0x23, 0x45, 0x67, 0x89, 0x01}
54 | assert.Equal(t, wantH, h)
55 |
56 | // Invalid: too long by one character / a nibble.
57 | s = "01234567890123456789012345678901012345678901234567890123456789000"
58 | h = TransactionHash{}
59 | err = h.UnmarshalText([]byte(s))
60 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 65 expected 64")
61 |
62 | // Invalid: too long by two characters / a byte.
63 | s = "012345678901234567890123456789010123456789012345678901234567890000"
64 | h = TransactionHash{}
65 | err = h.UnmarshalText([]byte(s))
66 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 66 expected 64")
67 |
68 | // Invalid: too short by one character / a nibble.
69 | s = "012345678901234567890123456789010123456789012345678901234567890"
70 | h = TransactionHash{}
71 | err = h.UnmarshalText([]byte(s))
72 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 63 expected 64")
73 |
74 | // Invalid: too short by two characters / a byte.
75 | s = "01234567890123456789012345678901012345678901234567890123456789"
76 | h = TransactionHash{}
77 | err = h.UnmarshalText([]byte(s))
78 | assert.EqualError(t, err, "unmarshaling transaction hash: input length 62 expected 64")
79 | }
80 |
--------------------------------------------------------------------------------
/sdk/state/integrationtests/helpers_test.go:
--------------------------------------------------------------------------------
1 | package integrationtests
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/binary"
6 | "testing"
7 | "time"
8 |
9 | stellarAmount "github.com/stellar/go/amount"
10 | "github.com/stellar/go/clients/horizonclient"
11 | "github.com/stellar/go/keypair"
12 | "github.com/stellar/go/protocols/horizon"
13 | "github.com/stellar/go/txnbuild"
14 | "github.com/stellar/starlight/sdk/state"
15 | "github.com/stellar/starlight/sdk/txbuild"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | // functions to be used in the sdk/state/integrationtests integration tests
20 |
21 | type AssetParam struct {
22 | Asset state.Asset
23 | Distributor *keypair.Full
24 | }
25 |
26 | func initAccounts(t *testing.T, assetParam AssetParam) (initiator Participant, responder Participant) {
27 | initiator = Participant{
28 | Name: "Initiator",
29 | KP: keypair.MustRandom(),
30 | ChannelAccount: keypair.MustRandom(),
31 | Contribution: 1_000_0000000,
32 | }
33 | t.Log("Initiator:", initiator.KP.Address())
34 | t.Log("Initiator ChannelAccount:", initiator.ChannelAccount.Address())
35 | {
36 | err := retry(t, 2, func() error { return createAccount(initiator.KP.FromAddress()) })
37 | require.NoError(t, err)
38 | err = retry(t, 2, func() error {
39 | return fundAsset(assetParam.Asset, initiator.Contribution, initiator.KP, assetParam.Distributor)
40 | })
41 | require.NoError(t, err)
42 |
43 | t.Log("Initiator Contribution:", initiator.Contribution, "of asset:", assetParam.Asset.Code(), "issuer: ", assetParam.Asset.Issuer())
44 | initChannelAccount(t, &initiator, assetParam)
45 | }
46 | t.Log("Initiator ChannelAccount Sequence Number:", initiator.ChannelAccountSequenceNumber)
47 |
48 | // Setup responder.
49 | responder = Participant{
50 | Name: "Responder",
51 | KP: keypair.MustRandom(),
52 | ChannelAccount: keypair.MustRandom(),
53 | Contribution: 1_000_0000000,
54 | }
55 | t.Log("Responder:", responder.KP.Address())
56 | t.Log("Responder ChannelAccount:", responder.ChannelAccount.Address())
57 | {
58 | err := retry(t, 2, func() error { return createAccount(responder.KP.FromAddress()) })
59 | require.NoError(t, err)
60 | err = retry(t, 2, func() error {
61 | return fundAsset(assetParam.Asset, responder.Contribution, responder.KP, assetParam.Distributor)
62 | })
63 | require.NoError(t, err)
64 |
65 | t.Log("Responder Contribution:", responder.Contribution, "of asset:", assetParam.Asset.Code(), "issuer: ", assetParam.Asset.Issuer())
66 | initChannelAccount(t, &responder, assetParam)
67 | }
68 | t.Log("Responder ChannelAccount Sequence Number:", responder.ChannelAccountSequenceNumber)
69 |
70 | return initiator, responder
71 | }
72 |
73 | func initChannelAccount(t *testing.T, participant *Participant, assetParam AssetParam) {
74 | // create channel account
75 | account, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: participant.KP.Address()})
76 | require.NoError(t, err)
77 | seqNum, err := account.GetSequenceNumber()
78 | require.NoError(t, err)
79 |
80 | tx, err := txbuild.CreateChannelAccount(txbuild.CreateChannelAccountParams{
81 | Creator: participant.KP.FromAddress(),
82 | ChannelAccount: participant.ChannelAccount.FromAddress(),
83 | SequenceNumber: seqNum + 1,
84 | Asset: assetParam.Asset.Asset(),
85 | })
86 | require.NoError(t, err)
87 | tx, err = tx.Sign(networkPassphrase, participant.KP, participant.ChannelAccount)
88 | require.NoError(t, err)
89 | fbtx, err := txnbuild.NewFeeBumpTransaction(txnbuild.FeeBumpTransactionParams{
90 | Inner: tx,
91 | FeeAccount: participant.KP.Address(),
92 | BaseFee: txnbuild.MinBaseFee,
93 | })
94 | require.NoError(t, err)
95 | fbtx, err = fbtx.Sign(networkPassphrase, participant.KP)
96 | require.NoError(t, err)
97 | var txResp horizon.Transaction
98 | err = retry(t, 2, func() error {
99 | txResp, err = client.SubmitFeeBumpTransaction(fbtx)
100 | return err
101 | })
102 | require.NoError(t, err)
103 | participant.ChannelAccountSequenceNumber = int64(txResp.Ledger) << 32
104 |
105 | // add initial contribution, use the same contribution for each asset
106 | _, err = account.IncrementSequenceNumber()
107 | require.NoError(t, err)
108 |
109 | payments := []txnbuild.Operation{
110 | &txnbuild.Payment{
111 | Destination: participant.ChannelAccount.Address(),
112 | Amount: stellarAmount.StringFromInt64(participant.Contribution),
113 | Asset: assetParam.Asset.Asset(),
114 | },
115 | }
116 |
117 | tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{
118 | SourceAccount: &account,
119 | BaseFee: txnbuild.MinBaseFee,
120 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)},
121 | IncrementSequenceNum: true,
122 | Operations: payments,
123 | })
124 | require.NoError(t, err)
125 |
126 | tx, err = tx.Sign(networkPassphrase, participant.KP)
127 | require.NoError(t, err)
128 | err = retry(t, 2, func() error {
129 | _, err = client.SubmitTransaction(tx)
130 | return err
131 | })
132 | require.NoError(t, err)
133 | }
134 |
135 | func initChannels(t *testing.T, initiator Participant, responder Participant) (initiatorChannel *state.Channel, responderChannel *state.Channel) {
136 | initiatorChannel = state.NewChannel(state.Config{
137 | NetworkPassphrase: networkPassphrase,
138 | MaxOpenExpiry: 5 * time.Minute,
139 | Initiator: true,
140 | LocalChannelAccount: initiator.ChannelAccount.FromAddress(),
141 | RemoteChannelAccount: responder.ChannelAccount.FromAddress(),
142 | LocalSigner: initiator.KP,
143 | RemoteSigner: responder.KP.FromAddress(),
144 | })
145 | responderChannel = state.NewChannel(state.Config{
146 | NetworkPassphrase: networkPassphrase,
147 | MaxOpenExpiry: 5 * time.Minute,
148 | Initiator: false,
149 | LocalChannelAccount: responder.ChannelAccount.FromAddress(),
150 | RemoteChannelAccount: initiator.ChannelAccount.FromAddress(),
151 | LocalSigner: responder.KP,
152 | RemoteSigner: initiator.KP.FromAddress(),
153 | })
154 | return initiatorChannel, responderChannel
155 | }
156 |
157 | func initAsset(t *testing.T, client horizonclient.ClientInterface, code string) (state.Asset, *keypair.Full) {
158 | issuerKP := keypair.MustRandom()
159 | distributorKP := keypair.MustRandom()
160 |
161 | err := retry(t, 2, func() error { return createAccount(issuerKP.FromAddress()) })
162 | require.NoError(t, err)
163 | err = retry(t, 2, func() error { return createAccount(distributorKP.FromAddress()) })
164 | require.NoError(t, err)
165 |
166 | var distributor horizon.Account
167 | err = retry(t, 2, func() error {
168 | distributor, err = client.AccountDetail(horizonclient.AccountRequest{AccountID: distributorKP.Address()})
169 | return err
170 | })
171 | require.NoError(t, err)
172 |
173 | asset := txnbuild.CreditAsset{Code: code, Issuer: issuerKP.Address()}
174 |
175 | tx, err := txnbuild.NewTransaction(
176 | txnbuild.TransactionParams{
177 | SourceAccount: &distributor,
178 | IncrementSequenceNum: true,
179 | BaseFee: txnbuild.MinBaseFee,
180 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()},
181 | Operations: []txnbuild.Operation{
182 | &txnbuild.ChangeTrust{
183 | Line: asset.MustToChangeTrustAsset(),
184 | Limit: "5000",
185 | },
186 | &txnbuild.Payment{
187 | Destination: distributorKP.Address(),
188 | Asset: asset,
189 | Amount: "5000",
190 | SourceAccount: issuerKP.Address(),
191 | },
192 | },
193 | },
194 | )
195 | require.NoError(t, err)
196 | tx, err = tx.Sign(networkPassphrase, distributorKP, issuerKP)
197 | require.NoError(t, err)
198 | err = retry(t, 2, func() error {
199 | _, err = client.SubmitTransaction(tx)
200 | return err
201 | })
202 | require.NoError(t, err)
203 |
204 | return state.Asset(asset.Code + ":" + asset.Issuer), distributorKP
205 | }
206 |
207 | func randomBool(t *testing.T) bool {
208 | t.Helper()
209 | b := [1]byte{}
210 | _, err := rand.Read(b[:])
211 | require.NoError(t, err)
212 | return b[0]%2 == 0
213 | }
214 |
215 | func randomPositiveInt64(t *testing.T, max int64) int64 {
216 | t.Helper()
217 | var i uint32
218 | err := binary.Read(rand.Reader, binary.LittleEndian, &i)
219 | require.NoError(t, err)
220 | return int64(i) % max
221 | }
222 |
223 | func retry(t *testing.T, maxAttempts int, f func() error) (err error) {
224 | t.Helper()
225 | for i := 0; i < maxAttempts; i++ {
226 | err = f()
227 | if err == nil {
228 | return
229 | }
230 | t.Logf("failed attempt %d at performing a retry-able operation: %v", i, err)
231 | time.Sleep(2 * time.Second)
232 | }
233 | return err
234 | }
235 |
236 | func fundAsset(asset state.Asset, amount int64, accountKP *keypair.Full, distributorKP *keypair.Full) error {
237 | distributor, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: distributorKP.Address()})
238 | if err != nil {
239 | return err
240 | }
241 |
242 | ops := []txnbuild.Operation{}
243 | if !asset.IsNative() {
244 | ops = append(ops, &txnbuild.ChangeTrust{
245 | SourceAccount: accountKP.Address(),
246 | Line: asset.Asset().MustToChangeTrustAsset(),
247 | Limit: "5000",
248 | })
249 | }
250 | ops = append(ops, &txnbuild.Payment{
251 | Destination: accountKP.Address(),
252 | Amount: stellarAmount.StringFromInt64(amount),
253 | Asset: asset.Asset(),
254 | })
255 | tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{
256 | SourceAccount: &distributor,
257 | IncrementSequenceNum: true,
258 | BaseFee: txnbuild.MinBaseFee,
259 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)},
260 | Operations: ops,
261 | })
262 | if err != nil {
263 | return err
264 | }
265 | if !asset.IsNative() {
266 | tx, err = tx.Sign(networkPassphrase, accountKP)
267 | if err != nil {
268 | return err
269 | }
270 | }
271 | tx, err = tx.Sign(networkPassphrase, distributorKP)
272 | if err != nil {
273 | return err
274 | }
275 | _, err = client.SubmitTransaction(tx)
276 | if err != nil {
277 | return err
278 | }
279 | return nil
280 | }
281 |
282 | func createAccount(account *keypair.FromAddress) error {
283 | // Try to fund via the fund endpoint first.
284 | _, err := client.Fund(account.Address())
285 | if err == nil {
286 | return nil
287 | }
288 |
289 | // Otherwise, fund via the root account.
290 | startingBalance := int64(1_000_0000000)
291 | rootResp, err := client.Root()
292 | if err != nil {
293 | return err
294 | }
295 | root := keypair.Master(rootResp.NetworkPassphrase).(*keypair.Full)
296 | sourceAccount, err := client.AccountDetail(horizonclient.AccountRequest{AccountID: root.Address()})
297 | if err != nil {
298 | return err
299 | }
300 | tx, err := txnbuild.NewTransaction(
301 | txnbuild.TransactionParams{
302 | SourceAccount: &sourceAccount,
303 | IncrementSequenceNum: true,
304 | BaseFee: txnbuild.MinBaseFee,
305 | Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)},
306 | Operations: []txnbuild.Operation{
307 | &txnbuild.CreateAccount{
308 | Destination: account.Address(),
309 | Amount: stellarAmount.StringFromInt64(startingBalance),
310 | },
311 | },
312 | },
313 | )
314 | if err != nil {
315 | return err
316 | }
317 | tx, err = tx.Sign(rootResp.NetworkPassphrase, root)
318 | if err != nil {
319 | return err
320 | }
321 | _, err = client.SubmitTransaction(tx)
322 | if err != nil {
323 | return err
324 | }
325 | return nil
326 | }
327 |
328 | func txSeqs(txs []*txnbuild.Transaction) []int64 {
329 | seqs := make([]int64, len(txs))
330 | for i := range txs {
331 | seqs[i] = txs[i].SequenceNumber()
332 | }
333 | return seqs
334 | }
335 |
336 | func assetBalance(asset state.Asset, account horizon.Account) string {
337 | for _, b := range account.Balances {
338 | if b.Asset.Code == asset.Code() {
339 | return b.Balance
340 | }
341 | }
342 | return "0"
343 | }
344 |
--------------------------------------------------------------------------------
/sdk/state/integrationtests/main_test.go:
--------------------------------------------------------------------------------
1 | package integrationtests
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "os"
7 | "testing"
8 | "time"
9 |
10 | "github.com/stellar/go/clients/horizonclient"
11 | )
12 |
13 | var horizonURL = func() string {
14 | url := os.Getenv("INTEGRATION_TESTS_HORIZON_URL")
15 | if url == "" {
16 | url = "https://horizon-testnet.stellar.org"
17 | }
18 | return url
19 | }()
20 |
21 | var (
22 | networkPassphrase string
23 | client *horizonclient.Client
24 | )
25 |
26 | func TestMain(m *testing.M) {
27 | if os.Getenv("INTEGRATION_TESTS") == "" {
28 | fmt.Fprintln(os.Stderr, "SKIP")
29 | os.Exit(0)
30 | }
31 |
32 | client = &horizonclient.Client{
33 | HorizonURL: horizonURL,
34 | HTTP: &http.Client{
35 | Timeout: 20 * time.Second,
36 | },
37 | }
38 | networkDetails, err := client.Root()
39 | if err != nil {
40 | fmt.Fprintf(os.Stderr, "error: %v\n", err)
41 | os.Exit(1)
42 | }
43 | networkPassphrase = networkDetails.NetworkPassphrase
44 |
45 | os.Exit(m.Run())
46 | }
47 |
--------------------------------------------------------------------------------
/sdk/state/state.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/stellar/go/keypair"
8 | "github.com/stellar/starlight/sdk/txbuild"
9 | )
10 |
11 | // Config contains the information for setting up a new channel.
12 | type Config struct {
13 | NetworkPassphrase string
14 | MaxOpenExpiry time.Duration
15 |
16 | Initiator bool
17 |
18 | LocalChannelAccount *keypair.FromAddress
19 | RemoteChannelAccount *keypair.FromAddress
20 |
21 | LocalSigner *keypair.Full
22 | RemoteSigner *keypair.FromAddress
23 | }
24 |
25 | // NewChannel constructs a new channel with the given config.
26 | func NewChannel(c Config) *Channel {
27 | channel := &Channel{
28 | networkPassphrase: c.NetworkPassphrase,
29 | maxOpenExpiry: c.MaxOpenExpiry,
30 | initiator: c.Initiator,
31 | localChannelAccount: &ChannelAccount{Address: c.LocalChannelAccount},
32 | remoteChannelAccount: &ChannelAccount{Address: c.RemoteChannelAccount},
33 | localSigner: c.LocalSigner,
34 | remoteSigner: c.RemoteSigner,
35 | }
36 | return channel
37 | }
38 |
39 | // Snapshot is a snapshot of a Channel's internal state. If a Snapshot is
40 | // combined with a Channel's initialization config they can be used to create a
41 | // new Channel that has the same state.
42 | type Snapshot struct {
43 | LocalChannelAccountSequence int64
44 | LocalChannelAccountBalance int64
45 | LocalChannelAccountLastSeenTransactionOrderID int64
46 | RemoteChannelAccountSequence int64
47 | RemoteChannelAccountBalance int64
48 | RemoteChannelAccountLastSeenTransactionOrderID int64
49 |
50 | OpenAgreement OpenAgreement
51 | OpenExecutedAndValidated bool
52 | OpenExecutedWithError bool
53 |
54 | LatestAuthorizedCloseAgreement CloseAgreement
55 | LatestUnauthorizedCloseAgreement CloseAgreement
56 | }
57 |
58 | // NewChannelFromSnapshot creates the channel with the given config, and
59 | // restores the internal state of the channel using the snapshot. To restore the
60 | // channel to its identical state the same config should be provided that was in
61 | // use when the snapshot was created.
62 | func NewChannelFromSnapshot(c Config, s Snapshot) *Channel {
63 | channel := NewChannel(c)
64 |
65 | channel.localChannelAccount.SequenceNumber = s.LocalChannelAccountSequence
66 | channel.localChannelAccount.Balance = s.LocalChannelAccountBalance
67 | channel.localChannelAccount.LastSeenTransactionOrderID = s.LocalChannelAccountLastSeenTransactionOrderID
68 | channel.remoteChannelAccount.SequenceNumber = s.RemoteChannelAccountSequence
69 | channel.remoteChannelAccount.Balance = s.RemoteChannelAccountBalance
70 | channel.remoteChannelAccount.LastSeenTransactionOrderID = s.RemoteChannelAccountLastSeenTransactionOrderID
71 |
72 | channel.openAgreement = s.OpenAgreement
73 | channel.openExecutedAndValidated = s.OpenExecutedAndValidated
74 | if s.OpenExecutedWithError {
75 | channel.openExecutedWithError = fmt.Errorf("open executed with error")
76 | }
77 |
78 | channel.latestAuthorizedCloseAgreement = s.LatestAuthorizedCloseAgreement
79 | channel.latestUnauthorizedCloseAgreement = s.LatestUnauthorizedCloseAgreement
80 |
81 | return channel
82 | }
83 |
84 | // ChannelAccount holds the details that a Channel tracks for a Stellar account.
85 | // A Channel tracks the details of two Stellar accounts, one for each
86 | // participant in the channel.
87 | //
88 | // The channel accounts hold the assets that are used for payments and will be
89 | // re-distributed at close.
90 | type ChannelAccount struct {
91 | Address *keypair.FromAddress
92 | SequenceNumber int64
93 | Balance int64
94 | LastSeenTransactionOrderID int64
95 | }
96 |
97 | // Channel holds the state of a single Starlight payment channel.
98 | type Channel struct {
99 | networkPassphrase string
100 | maxOpenExpiry time.Duration
101 |
102 | initiator bool
103 | localChannelAccount *ChannelAccount
104 | remoteChannelAccount *ChannelAccount
105 |
106 | localSigner *keypair.Full
107 | remoteSigner *keypair.FromAddress
108 |
109 | openAgreement OpenAgreement
110 | openExecutedAndValidated bool
111 | openExecutedWithError error
112 |
113 | latestAuthorizedCloseAgreement CloseAgreement
114 | latestUnauthorizedCloseAgreement CloseAgreement
115 | }
116 |
117 | // Snapshot returns a snapshot of the channel's internal state that if combined
118 | // with it's initialization config can be used to create a new Channel that has
119 | // the same state.
120 | func (c *Channel) Snapshot() Snapshot {
121 | return Snapshot{
122 | LocalChannelAccountSequence: c.localChannelAccount.SequenceNumber,
123 | LocalChannelAccountBalance: c.localChannelAccount.Balance,
124 | LocalChannelAccountLastSeenTransactionOrderID: c.localChannelAccount.LastSeenTransactionOrderID,
125 | RemoteChannelAccountSequence: c.remoteChannelAccount.SequenceNumber,
126 | RemoteChannelAccountBalance: c.remoteChannelAccount.Balance,
127 | RemoteChannelAccountLastSeenTransactionOrderID: c.remoteChannelAccount.LastSeenTransactionOrderID,
128 |
129 | OpenAgreement: c.openAgreement,
130 | OpenExecutedAndValidated: c.openExecutedAndValidated,
131 | OpenExecutedWithError: c.openExecutedWithError != nil,
132 |
133 | LatestAuthorizedCloseAgreement: c.latestAuthorizedCloseAgreement,
134 | LatestUnauthorizedCloseAgreement: c.latestUnauthorizedCloseAgreement,
135 | }
136 | }
137 |
138 | type State int
139 |
140 | const (
141 | StateError State = iota - 1
142 | StateNone
143 | StateOpen
144 | StateClosingWithOutdatedState
145 | StateClosedWithOutdatedState
146 | StateClosing
147 | StateClosed
148 | )
149 |
150 | // State returns a single value representing the overall state of the
151 | // channel. If there was an error finding the state, or internal values are
152 | // unexpected, then a failed channel state is returned, indicating something is
153 | // wrong.
154 | func (c *Channel) State() (State, error) {
155 | if c.openExecutedWithError != nil {
156 | return StateError, nil
157 | }
158 |
159 | if !c.openExecutedAndValidated {
160 | return StateNone, nil
161 | }
162 |
163 | // Get the sequence numbers for the latest close agreement transactions.
164 | txs, err := c.closeTxs(c.openAgreement.Envelope.Details, c.latestAuthorizedCloseAgreement.Envelope.Details)
165 | if err != nil {
166 | return -1, fmt.Errorf("building declaration and close txs for latest authorized close agreement: %w", err)
167 | }
168 | latestDeclSequence := txs.Declaration.SequenceNumber()
169 | latestCloseSequence := txs.Close.SequenceNumber()
170 |
171 | initiatorChannelAccountSeqNum := c.initiatorChannelAccount().SequenceNumber
172 | s := c.openAgreement.Envelope.Details.StartingSequence
173 |
174 | if initiatorChannelAccountSeqNum == s {
175 | return StateOpen, nil
176 | } else if initiatorChannelAccountSeqNum < latestDeclSequence &&
177 | txbuild.SequenceNumberToTransactionType(s, initiatorChannelAccountSeqNum) == txbuild.TransactionTypeDeclaration {
178 | return StateClosingWithOutdatedState, nil
179 | } else if initiatorChannelAccountSeqNum < latestDeclSequence &&
180 | txbuild.SequenceNumberToTransactionType(s, initiatorChannelAccountSeqNum) == txbuild.TransactionTypeClose {
181 | return StateClosedWithOutdatedState, nil
182 | } else if initiatorChannelAccountSeqNum == latestDeclSequence {
183 | return StateClosing, nil
184 | } else if initiatorChannelAccountSeqNum >= latestCloseSequence {
185 | return StateClosed, nil
186 | }
187 |
188 | return StateError, fmt.Errorf("initiator channel account sequence has unexpected value")
189 | }
190 |
191 | func (c *Channel) setInitiatorChannelAccountSequence(seqNum int64) {
192 | c.initiatorChannelAccount().SequenceNumber = seqNum
193 | }
194 |
195 | // IsInitiator returns true if this channel initiated the process of opening a
196 | // channel, else false.
197 | func (c *Channel) IsInitiator() bool {
198 | return c.initiator
199 | }
200 |
201 | // nextIterationNumber returns the next iteration number for the channel. If
202 | // there is a pending unauthorized close agreement, then that agreement
203 | // iteration is used, else the latest authorized agreeement is used.
204 | func (c *Channel) nextIterationNumber() int64 {
205 | if !c.latestUnauthorizedCloseAgreement.Envelope.Empty() {
206 | return c.latestUnauthorizedCloseAgreement.Envelope.Details.IterationNumber
207 | }
208 | return c.latestAuthorizedCloseAgreement.Envelope.Details.IterationNumber + 1
209 | }
210 |
211 | // Balance returns the amount owing from the initiator to the responder, if positive, or
212 | // the amount owing from the responder to the initiator, if negative.
213 | func (c *Channel) Balance() int64 {
214 | return c.latestAuthorizedCloseAgreement.Envelope.Details.Balance
215 | }
216 |
217 | // OpenAgreement returns the open agreement used to open the channel.
218 | func (c *Channel) OpenAgreement() OpenAgreement {
219 | return c.openAgreement
220 | }
221 |
222 | // LatestCloseAgreement returns the latest close agreement signed by both
223 | // channel participants.
224 | func (c *Channel) LatestCloseAgreement() CloseAgreement {
225 | return c.latestAuthorizedCloseAgreement
226 | }
227 |
228 | // LatestUnauthorizedCloseAgreement returns the latest unauthorized close
229 | // agreement yet to be signed by both participants.
230 | func (c *Channel) LatestUnauthorizedCloseAgreement() (CloseAgreement, bool) {
231 | return c.latestUnauthorizedCloseAgreement, !c.latestUnauthorizedCloseAgreement.Envelope.Empty()
232 | }
233 |
234 | // UpdateLocalChannelAccountBalance updates the local channel account balance.
235 | func (c *Channel) UpdateLocalChannelAccountBalance(balance int64) {
236 | c.localChannelAccount.Balance = balance
237 | }
238 |
239 | // UpdateRemoteChannelAccountBalance updates the remote channel account balance.
240 | func (c *Channel) UpdateRemoteChannelAccountBalance(balance int64) {
241 | c.remoteChannelAccount.Balance = balance
242 | }
243 |
244 | // LocalChannelAccount returns the local channel account.
245 | func (c *Channel) LocalChannelAccount() ChannelAccount {
246 | return *c.localChannelAccount
247 | }
248 |
249 | // RemoteChannelAccount returns the remote channel account.
250 | func (c *Channel) RemoteChannelAccount() ChannelAccount {
251 | return *c.remoteChannelAccount
252 | }
253 |
254 | func (c *Channel) initiatorChannelAccount() *ChannelAccount {
255 | if c.initiator {
256 | return c.localChannelAccount
257 | } else {
258 | return c.remoteChannelAccount
259 | }
260 | }
261 |
262 | func (c *Channel) responderChannelAccount() *ChannelAccount {
263 | if c.initiator {
264 | return c.remoteChannelAccount
265 | } else {
266 | return c.localChannelAccount
267 | }
268 | }
269 |
270 | func (c *Channel) initiatorSigner() *keypair.FromAddress {
271 | if c.initiator {
272 | return c.localSigner.FromAddress()
273 | } else {
274 | return c.remoteSigner
275 | }
276 | }
277 |
278 | func (c *Channel) responderSigner() *keypair.FromAddress {
279 | if c.initiator {
280 | return c.remoteSigner
281 | } else {
282 | return c.localSigner.FromAddress()
283 | }
284 | }
285 |
286 | func (c *Channel) amountToLocal(balance int64) int64 {
287 | if c.initiator {
288 | return amountToInitiator(balance)
289 | }
290 | return amountToResponder(balance)
291 | }
292 |
293 | func (c *Channel) amountToRemote(balance int64) int64 {
294 | if c.initiator {
295 | return amountToResponder(balance)
296 | }
297 | return amountToInitiator(balance)
298 | }
299 |
300 | func amountToInitiator(balance int64) int64 {
301 | if balance < 0 {
302 | return balance * -1
303 | }
304 | return 0
305 | }
306 |
307 | func amountToResponder(balance int64) int64 {
308 | if balance > 0 {
309 | return balance
310 | }
311 | return 0
312 | }
313 |
--------------------------------------------------------------------------------
/sdk/state/state_test.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stellar/go/keypair"
9 | "github.com/stellar/go/network"
10 | "github.com/stellar/go/txnbuild"
11 | "github.com/stellar/go/xdr"
12 | "github.com/stellar/starlight/sdk/txbuild/txbuildtest"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | func assertChannelSnapshotsAndRestores(t *testing.T, config Config, channel *Channel) {
18 | t.Helper()
19 |
20 | snapshot := channel.Snapshot()
21 | snapshotJSON, err := json.Marshal(snapshot)
22 | require.NoError(t, err)
23 | restoredSnapshot := Snapshot{}
24 | err = json.Unmarshal(snapshotJSON, &restoredSnapshot)
25 | require.NoError(t, err)
26 |
27 | restoredChannel := NewChannelFromSnapshot(config, snapshot)
28 |
29 | require.Equal(t, channel, restoredChannel)
30 | }
31 |
32 | func TestNewChannelWithSnapshot(t *testing.T) {
33 | localSigner := keypair.MustRandom()
34 | remoteSigner := keypair.MustRandom()
35 | localChannelAccount := keypair.MustRandom().FromAddress()
36 | remoteChannelAccount := keypair.MustRandom().FromAddress()
37 |
38 | localConfig := Config{
39 | NetworkPassphrase: network.TestNetworkPassphrase,
40 | Initiator: true,
41 | LocalSigner: localSigner,
42 | RemoteSigner: remoteSigner.FromAddress(),
43 | LocalChannelAccount: localChannelAccount,
44 | RemoteChannelAccount: remoteChannelAccount,
45 | MaxOpenExpiry: 2 * time.Hour,
46 | }
47 | localChannel := NewChannel(localConfig)
48 | remoteConfig := Config{
49 | NetworkPassphrase: network.TestNetworkPassphrase,
50 | Initiator: false,
51 | LocalSigner: remoteSigner,
52 | RemoteSigner: localSigner.FromAddress(),
53 | LocalChannelAccount: remoteChannelAccount,
54 | RemoteChannelAccount: localChannelAccount,
55 | MaxOpenExpiry: 2 * time.Hour,
56 | }
57 | remoteChannel := NewChannel(remoteConfig)
58 |
59 | // Check snapshot rehydrates the channel identically when new and not open.
60 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
61 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
62 |
63 | // Negotiate the open state.
64 | {
65 | open1, err := localChannel.ProposeOpen(OpenParams{
66 | ObservationPeriodTime: 1,
67 | ObservationPeriodLedgerGap: 1,
68 | ExpiresAt: time.Now().Add(time.Hour),
69 | StartingSequence: 101,
70 | })
71 | require.NoError(t, err)
72 | open2, err := remoteChannel.ConfirmOpen(open1.Envelope)
73 | require.NoError(t, err)
74 | _, err = localChannel.ConfirmOpen(open2.Envelope)
75 | require.NoError(t, err)
76 | }
77 |
78 | // Check snapshot rehydrates the channel identically when open.
79 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
80 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
81 |
82 | // Put the channel into an open state by ingesting the open tx.
83 | {
84 | ftx, err := localChannel.OpenTx()
85 | require.NoError(t, err)
86 | ftxXDR, err := ftx.Base64()
87 | require.NoError(t, err)
88 |
89 | successResultXDR, err := txbuildtest.BuildResultXDR(true)
90 | require.NoError(t, err)
91 | resultMetaXDR, err := txbuildtest.BuildOpenResultMetaXDR(txbuildtest.OpenResultMetaParams{
92 | InitiatorSigner: localSigner.Address(),
93 | ResponderSigner: remoteSigner.Address(),
94 | InitiatorChannelAccount: localChannelAccount.Address(),
95 | ResponderChannelAccount: remoteChannelAccount.Address(),
96 | StartSequence: 101,
97 | Asset: txnbuild.NativeAsset{},
98 | })
99 | require.NoError(t, err)
100 |
101 | err = localChannel.IngestTx(1, ftxXDR, successResultXDR, resultMetaXDR)
102 | require.NoError(t, err)
103 | err = remoteChannel.IngestTx(1, ftxXDR, successResultXDR, resultMetaXDR)
104 | require.NoError(t, err)
105 |
106 | cs, err := localChannel.State()
107 | require.NoError(t, err)
108 | assert.Equal(t, StateOpen, cs)
109 |
110 | cs, err = remoteChannel.State()
111 | require.NoError(t, err)
112 | assert.Equal(t, StateOpen, cs)
113 | }
114 |
115 | // Check snapshot rehydrates the channel identically when open.
116 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
117 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
118 |
119 | // Update balances.
120 | localChannel.UpdateLocalChannelAccountBalance(100)
121 | localChannel.UpdateRemoteChannelAccountBalance(200)
122 | remoteChannel.UpdateLocalChannelAccountBalance(300)
123 | remoteChannel.UpdateRemoteChannelAccountBalance(400)
124 |
125 | // Check snapshot rehydrates the channel identically when open and with
126 | // balances updated.
127 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
128 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
129 |
130 | // Propose a payment.
131 | ca, err := localChannel.ProposePayment(10)
132 | require.NoError(t, err)
133 |
134 | // Check snapshot rehydrates the channel identically when payment proposed.
135 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
136 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
137 |
138 | // Confirm the payment.
139 | ca, err = remoteChannel.ConfirmPayment(ca.Envelope)
140 | require.NoError(t, err)
141 | _, err = localChannel.ConfirmPayment(ca.Envelope)
142 | require.NoError(t, err)
143 |
144 | // Check snapshot rehydrates the channel identically when payment confirmed.
145 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
146 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
147 |
148 | // Propose a close.
149 | ca, err = localChannel.ProposeClose()
150 | require.NoError(t, err)
151 |
152 | // Check snapshot rehydrates the channel identically when payment confirmed.
153 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
154 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
155 |
156 | // Put the channel into a closing state by ingesting the closing declaration tx.
157 | {
158 | dtx, _, err := localChannel.CloseTxs()
159 | require.NoError(t, err)
160 | dtxXDR, err := dtx.Base64()
161 | require.NoError(t, err)
162 |
163 | successResultXDR, err := txbuildtest.BuildResultXDR(true)
164 | require.NoError(t, err)
165 |
166 | resultMetaXDR, err := txbuildtest.BuildResultMetaXDR([]xdr.LedgerEntryData{
167 | {
168 | Type: xdr.LedgerEntryTypeAccount,
169 | Account: &xdr.AccountEntry{
170 | AccountId: xdr.MustAddress(localChannelAccount.Address()),
171 | SeqNum: 102,
172 | Signers: []xdr.Signer{
173 | {Key: xdr.MustSigner(localSigner.Address()), Weight: 1},
174 | {Key: xdr.MustSigner(remoteSigner.Address()), Weight: 1},
175 | },
176 | Thresholds: xdr.Thresholds{0, 2, 2, 2},
177 | },
178 | },
179 | {
180 | Type: xdr.LedgerEntryTypeAccount,
181 | Account: &xdr.AccountEntry{
182 | AccountId: xdr.MustAddress(remoteChannelAccount.Address()),
183 | SeqNum: 103,
184 | Signers: []xdr.Signer{
185 | {Key: xdr.MustSigner(remoteSigner.Address()), Weight: 1},
186 | {Key: xdr.MustSigner(localSigner.Address()), Weight: 1},
187 | },
188 | Thresholds: xdr.Thresholds{0, 2, 2, 2},
189 | },
190 | },
191 | })
192 | require.NoError(t, err)
193 |
194 | err = localChannel.IngestTx(1, dtxXDR, successResultXDR, resultMetaXDR)
195 | require.NoError(t, err)
196 | err = remoteChannel.IngestTx(1, dtxXDR, successResultXDR, resultMetaXDR)
197 | require.NoError(t, err)
198 |
199 | cs, err := localChannel.State()
200 | require.NoError(t, err)
201 | assert.Equal(t, StateClosing, cs)
202 |
203 | cs, err = remoteChannel.State()
204 | require.NoError(t, err)
205 | assert.Equal(t, StateClosing, cs)
206 | }
207 |
208 | // Check snapshot rehydrates the channel identically when closing ingested.
209 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
210 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
211 |
212 | // Confirm the close.
213 | ca, err = remoteChannel.ConfirmClose(ca.Envelope)
214 | require.NoError(t, err)
215 | _, err = localChannel.ConfirmClose(ca.Envelope)
216 | require.NoError(t, err)
217 |
218 | // Check snapshot rehydrates the channel identically when payment confirmed.
219 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
220 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
221 |
222 | // Put the channel into a close state by ingesting the close tx.
223 | {
224 | _, ctx, err := localChannel.CloseTxs()
225 | require.NoError(t, err)
226 | ctxXDR, err := ctx.Base64()
227 | require.NoError(t, err)
228 |
229 | successResultXDR, err := txbuildtest.BuildResultXDR(true)
230 | require.NoError(t, err)
231 |
232 | resultMetaXDR, err := txbuildtest.BuildResultMetaXDR([]xdr.LedgerEntryData{})
233 | require.NoError(t, err)
234 |
235 | err = localChannel.IngestTx(1, ctxXDR, successResultXDR, resultMetaXDR)
236 | require.NoError(t, err)
237 | err = remoteChannel.IngestTx(1, ctxXDR, successResultXDR, resultMetaXDR)
238 | require.NoError(t, err)
239 |
240 | cs, err := localChannel.State()
241 | require.NoError(t, err)
242 | assert.Equal(t, StateClosed, cs)
243 |
244 | cs, err = remoteChannel.State()
245 | require.NoError(t, err)
246 | assert.Equal(t, StateClosed, cs)
247 | }
248 |
249 | // Check snapshot rehydrates the channel identically when payment confirmed.
250 | assertChannelSnapshotsAndRestores(t, localConfig, localChannel)
251 | assertChannelSnapshotsAndRestores(t, remoteConfig, remoteChannel)
252 | }
253 |
--------------------------------------------------------------------------------
/sdk/state/verify.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "github.com/stellar/go/keypair"
5 | "github.com/stellar/go/xdr"
6 | "golang.org/x/sync/errgroup"
7 | )
8 |
9 | type signatureVerificationInput struct {
10 | TransactionHash TransactionHash
11 | Signature xdr.Signature
12 | Signer *keypair.FromAddress
13 | }
14 |
15 | func verifySignatures(inputs []signatureVerificationInput) error {
16 | g := errgroup.Group{}
17 | for _, i := range inputs {
18 | i := i
19 | g.Go(func() error {
20 | return i.Signer.Verify(i.TransactionHash[:], []byte(i.Signature))
21 | })
22 | }
23 | return g.Wait()
24 | }
25 |
--------------------------------------------------------------------------------
/sdk/txbuild/close.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/stellar/go/amount"
8 | "github.com/stellar/go/keypair"
9 | "github.com/stellar/go/txnbuild"
10 | )
11 |
12 | type CloseParams struct {
13 | ObservationPeriodTime time.Duration
14 | ObservationPeriodLedgerGap uint32
15 | InitiatorSigner *keypair.FromAddress
16 | ResponderSigner *keypair.FromAddress
17 | InitiatorChannelAccount *keypair.FromAddress
18 | ResponderChannelAccount *keypair.FromAddress
19 | StartSequence int64
20 | IterationNumber int64
21 | AmountToInitiator int64
22 | AmountToResponder int64
23 | Asset txnbuild.Asset
24 | }
25 |
26 | func Close(p CloseParams) (*txnbuild.Transaction, error) {
27 | if p.IterationNumber < 0 || p.StartSequence <= 0 {
28 | return nil, fmt.Errorf("invalid iteration number or start sequence: cannot be negative")
29 | }
30 |
31 | // Close is the second transaction in an iteration's transaction set.
32 | seq := startSequenceOfIteration(p.StartSequence, p.IterationNumber) + 1
33 | if seq < 0 {
34 | return nil, fmt.Errorf("invalid sequence number: cannot be negative")
35 | }
36 |
37 | tp := txnbuild.TransactionParams{
38 | SourceAccount: &txnbuild.SimpleAccount{
39 | AccountID: p.InitiatorChannelAccount.Address(),
40 | Sequence: seq,
41 | },
42 | BaseFee: 0,
43 | Preconditions: txnbuild.Preconditions{
44 | TimeBounds: txnbuild.NewInfiniteTimeout(),
45 | MinSequenceNumberAge: uint64(p.ObservationPeriodTime.Seconds()),
46 | MinSequenceNumberLedgerGap: p.ObservationPeriodLedgerGap,
47 | },
48 | Operations: []txnbuild.Operation{
49 | &txnbuild.SetOptions{
50 | SourceAccount: p.InitiatorChannelAccount.Address(),
51 | MasterWeight: txnbuild.NewThreshold(0),
52 | LowThreshold: txnbuild.NewThreshold(1),
53 | MediumThreshold: txnbuild.NewThreshold(1),
54 | HighThreshold: txnbuild.NewThreshold(1),
55 | Signer: &txnbuild.Signer{Address: p.ResponderSigner.Address(), Weight: 0},
56 | },
57 | &txnbuild.SetOptions{
58 | SourceAccount: p.ResponderChannelAccount.Address(),
59 | MasterWeight: txnbuild.NewThreshold(0),
60 | LowThreshold: txnbuild.NewThreshold(1),
61 | MediumThreshold: txnbuild.NewThreshold(1),
62 | HighThreshold: txnbuild.NewThreshold(1),
63 | Signer: &txnbuild.Signer{Address: p.InitiatorSigner.Address(), Weight: 0},
64 | },
65 | },
66 | }
67 | if p.AmountToInitiator != 0 {
68 | tp.Operations = append(tp.Operations, &txnbuild.Payment{
69 | SourceAccount: p.ResponderChannelAccount.Address(),
70 | Destination: p.InitiatorChannelAccount.Address(),
71 | Asset: p.Asset,
72 | Amount: amount.StringFromInt64(p.AmountToInitiator),
73 | })
74 | }
75 | if p.AmountToResponder != 0 {
76 | tp.Operations = append(tp.Operations, &txnbuild.Payment{
77 | SourceAccount: p.InitiatorChannelAccount.Address(),
78 | Destination: p.ResponderChannelAccount.Address(),
79 | Asset: p.Asset,
80 | Amount: amount.StringFromInt64(p.AmountToResponder),
81 | })
82 | }
83 | tx, err := txnbuild.NewTransaction(tp)
84 | if err != nil {
85 | return nil, err
86 | }
87 | return tx, nil
88 | }
89 |
--------------------------------------------------------------------------------
/sdk/txbuild/close_test.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "encoding/base64"
5 | "math"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stellar/go/keypair"
10 | "github.com/stellar/go/txnbuild"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestClose_size(t *testing.T) {
16 | initiatorSigner := keypair.MustRandom()
17 | responderSigner := keypair.MustRandom()
18 | initiatorChannelAccount := keypair.MustRandom()
19 | responderChannelAccount := keypair.MustRandom()
20 |
21 | tx, err := Close(CloseParams{
22 | ObservationPeriodTime: 5 * time.Minute,
23 | ObservationPeriodLedgerGap: 5,
24 | InitiatorSigner: initiatorSigner.FromAddress(),
25 | ResponderSigner: responderSigner.FromAddress(),
26 | InitiatorChannelAccount: initiatorChannelAccount.FromAddress(),
27 | ResponderChannelAccount: responderChannelAccount.FromAddress(),
28 | StartSequence: 101,
29 | IterationNumber: 1,
30 | AmountToInitiator: 100,
31 | AmountToResponder: 200,
32 | Asset: txnbuild.CreditAsset{Code: "ETH", Issuer: "GBTYEE5BTST64JCBUXVAEEPQJAY3TNV47A5JFUMQKNDWUJRRT6LUVEQH"},
33 | })
34 | require.NoError(t, err)
35 |
36 | // Test the size without signers.
37 | {
38 | txb, err := tx.MarshalBinary()
39 | require.NoError(t, err)
40 | t.Log("unsigned:", base64.StdEncoding.EncodeToString(txb))
41 | assert.Len(t, txb, 652)
42 | }
43 |
44 | // Test the size with signers.
45 | {
46 | tx, err := tx.Sign("test", initiatorSigner, responderSigner)
47 | require.NoError(t, err)
48 | txb, err := tx.MarshalBinary()
49 | require.NoError(t, err)
50 | t.Log("signed:", base64.StdEncoding.EncodeToString(txb))
51 | assert.Len(t, txb, 796)
52 | }
53 | }
54 |
55 | func TestClose_iterationNumber_checkNonNegative(t *testing.T) {
56 | _, err := Close(CloseParams{
57 | StartSequence: 101,
58 | IterationNumber: -1,
59 | })
60 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative")
61 | _, err = Close(CloseParams{
62 | StartSequence: -1,
63 | IterationNumber: 5,
64 | })
65 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative")
66 | }
67 |
68 | func TestClose_startSequenceOfIteration_checkNonNegative(t *testing.T) {
69 | _, err := Close(CloseParams{
70 | IterationNumber: 0,
71 | StartSequence: math.MaxInt64,
72 | })
73 | assert.EqualError(t, err, "invalid sequence number: cannot be negative")
74 | }
75 |
--------------------------------------------------------------------------------
/sdk/txbuild/create_channelaccount.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/stellar/go/amount"
7 | "github.com/stellar/go/keypair"
8 | "github.com/stellar/go/txnbuild"
9 | )
10 |
11 | type CreateChannelAccountParams struct {
12 | Creator *keypair.FromAddress
13 | ChannelAccount *keypair.FromAddress
14 | SequenceNumber int64
15 | Asset txnbuild.BasicAsset
16 | }
17 |
18 | func CreateChannelAccount(p CreateChannelAccountParams) (*txnbuild.Transaction, error) {
19 | ops := []txnbuild.Operation{
20 | &txnbuild.BeginSponsoringFutureReserves{
21 | SponsoredID: p.ChannelAccount.Address(),
22 | },
23 | &txnbuild.CreateAccount{
24 | Destination: p.ChannelAccount.Address(),
25 | // base reserves sponsored by p.Creator
26 | Amount: "0",
27 | },
28 | &txnbuild.SetOptions{
29 | SourceAccount: p.ChannelAccount.Address(),
30 | MasterWeight: txnbuild.NewThreshold(0),
31 | LowThreshold: txnbuild.NewThreshold(1),
32 | MediumThreshold: txnbuild.NewThreshold(1),
33 | HighThreshold: txnbuild.NewThreshold(1),
34 | Signer: &txnbuild.Signer{Address: p.Creator.Address(), Weight: 1},
35 | },
36 | }
37 | if !p.Asset.IsNative() {
38 | ops = append(ops, &txnbuild.ChangeTrust{
39 | Line: p.Asset.MustToChangeTrustAsset(),
40 | Limit: amount.StringFromInt64(math.MaxInt64),
41 | SourceAccount: p.ChannelAccount.Address(),
42 | })
43 | }
44 | ops = append(ops, &txnbuild.EndSponsoringFutureReserves{
45 | SourceAccount: p.ChannelAccount.Address(),
46 | })
47 |
48 | tx, err := txnbuild.NewTransaction(
49 | txnbuild.TransactionParams{
50 | SourceAccount: &txnbuild.SimpleAccount{
51 | AccountID: p.Creator.Address(),
52 | Sequence: p.SequenceNumber,
53 | },
54 | BaseFee: 0,
55 | Preconditions: txnbuild.Preconditions{
56 | TimeBounds: txnbuild.NewTimeout(300),
57 | },
58 | Operations: ops,
59 | },
60 | )
61 | if err != nil {
62 | return nil, err
63 | }
64 | return tx, nil
65 | }
66 |
--------------------------------------------------------------------------------
/sdk/txbuild/declaration.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/stellar/go/keypair"
7 | "github.com/stellar/go/strkey"
8 | "github.com/stellar/go/txnbuild"
9 | )
10 |
11 | type DeclarationParams struct {
12 | InitiatorChannelAccount *keypair.FromAddress
13 | StartSequence int64
14 | IterationNumber int64
15 | IterationNumberExecuted int64
16 | CloseTxHash [32]byte
17 | ConfirmingSigner *keypair.FromAddress
18 | }
19 |
20 | func Declaration(p DeclarationParams) (*txnbuild.Transaction, error) {
21 | if p.IterationNumber < 0 || p.StartSequence <= 0 {
22 | return nil, fmt.Errorf("invalid iteration number or start sequence: cannot be negative")
23 | }
24 |
25 | // Declaration is the first transaction in an iteration's transaction set.
26 | seq := startSequenceOfIteration(p.StartSequence, p.IterationNumber) + 0
27 | if seq < 0 {
28 | return nil, fmt.Errorf("invalid sequence number: cannot be negative")
29 | }
30 |
31 | minSequenceNumber := startSequenceOfIteration(p.StartSequence, p.IterationNumberExecuted)
32 |
33 | // Build the extra signature required for signing the declaration
34 | // transaction that will be required in addition to the signers for the
35 | // account signers. The extra signer will be a signature by the confirming
36 | // signer for the close transaction so that the confirming signer must
37 | // reveal that signature publicly when submitting the declaration
38 | // transaction. This prevents the confirming signer from withholding
39 | // signatures for the closing transactions.
40 | extraSigner, err := strkey.NewSignedPayload(p.ConfirmingSigner.Address(), p.CloseTxHash[:])
41 | if err != nil {
42 | return nil, err
43 | }
44 | extraSignerStr, err := extraSigner.Encode()
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | tp := txnbuild.TransactionParams{
50 | SourceAccount: &txnbuild.SimpleAccount{
51 | AccountID: p.InitiatorChannelAccount.Address(),
52 | Sequence: seq,
53 | },
54 | BaseFee: 0,
55 | Preconditions: txnbuild.Preconditions{
56 | TimeBounds: txnbuild.NewInfiniteTimeout(),
57 | MinSequenceNumber: &minSequenceNumber,
58 | ExtraSigners: []string{
59 | extraSignerStr,
60 | },
61 | },
62 | Operations: []txnbuild.Operation{
63 | &txnbuild.BumpSequence{
64 | BumpTo: 0,
65 | },
66 | },
67 | }
68 | tx, err := txnbuild.NewTransaction(tp)
69 | if err != nil {
70 | return nil, err
71 | }
72 | return tx, nil
73 | }
74 |
--------------------------------------------------------------------------------
/sdk/txbuild/declaration_test.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "encoding/base64"
5 | "math"
6 | "testing"
7 |
8 | "github.com/stellar/go/keypair"
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestDeclaration_size(t *testing.T) {
14 | initiatorSigner := keypair.MustRandom()
15 | responderSigner := keypair.MustRandom()
16 | initiatorChannelAccount := keypair.MustRandom()
17 |
18 | closeTxHash := [32]byte{}
19 | tx, err := Declaration(DeclarationParams{
20 | InitiatorChannelAccount: initiatorChannelAccount.FromAddress(),
21 | StartSequence: 101,
22 | IterationNumber: 1,
23 | CloseTxHash: closeTxHash,
24 | ConfirmingSigner: initiatorSigner.FromAddress(),
25 | })
26 | require.NoError(t, err)
27 |
28 | // Test the size without signers.
29 | {
30 | txb, err := tx.MarshalBinary()
31 | require.NoError(t, err)
32 | t.Log("unsigned:", base64.StdEncoding.EncodeToString(txb))
33 | assert.Len(t, txb, 212)
34 | }
35 |
36 | // Test the size with signers.
37 | {
38 | tx, err := tx.Sign("test", initiatorSigner, responderSigner)
39 | require.NoError(t, err)
40 | signedPayloadSig, err := responderSigner.SignPayloadDecorated(closeTxHash[:])
41 | require.NoError(t, err)
42 | tx, err = tx.AddSignatureDecorated(signedPayloadSig)
43 | require.NoError(t, err)
44 | txb, err := tx.MarshalBinary()
45 | require.NoError(t, err)
46 | t.Log("signed:", base64.StdEncoding.EncodeToString(txb))
47 | assert.Len(t, txb, 428)
48 | }
49 | }
50 |
51 | func TestDeclaration_iterationNumber_checkNonNegative(t *testing.T) {
52 | _, err := Declaration(DeclarationParams{
53 | StartSequence: 101,
54 | IterationNumber: -1,
55 | })
56 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative")
57 | _, err = Declaration(DeclarationParams{
58 | StartSequence: -1,
59 | IterationNumber: 5,
60 | })
61 | assert.EqualError(t, err, "invalid iteration number or start sequence: cannot be negative")
62 | }
63 |
64 | func TestDeclaration_startSequenceOfIteration_checkNonNegative(t *testing.T) {
65 | _, err := Declaration(DeclarationParams{
66 | IterationNumber: 1,
67 | StartSequence: math.MaxInt64,
68 | })
69 | assert.EqualError(t, err, "invalid sequence number: cannot be negative")
70 | }
71 |
--------------------------------------------------------------------------------
/sdk/txbuild/doc.go:
--------------------------------------------------------------------------------
1 | // Package txbuild houses the functions for creating transactions
2 | // to create, interact with, and finally close a Starlight channel.
3 | package txbuild
4 |
--------------------------------------------------------------------------------
/sdk/txbuild/open.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "math"
5 | "time"
6 |
7 | "github.com/stellar/go/amount"
8 | "github.com/stellar/go/keypair"
9 | "github.com/stellar/go/strkey"
10 | "github.com/stellar/go/txnbuild"
11 | )
12 |
13 | type OpenParams struct {
14 | InitiatorSigner *keypair.FromAddress
15 | ResponderSigner *keypair.FromAddress
16 | InitiatorChannelAccount *keypair.FromAddress
17 | ResponderChannelAccount *keypair.FromAddress
18 | StartSequence int64
19 | Asset txnbuild.Asset
20 | ExpiresAt time.Time
21 | DeclarationTxHash [32]byte
22 | CloseTxHash [32]byte
23 | ConfirmingSigner *keypair.FromAddress
24 | }
25 |
26 | func Open(p OpenParams) (*txnbuild.Transaction, error) {
27 | // Build the list of extra signatures required for signing the open
28 | // transaction that will be required in addition to the signers for the
29 | // account signers. The extra signers will be signatures by the confirming
30 | // signer for the declaration and close transaction so that the confirming
31 | // signer must reveal those signatures publicly when submitting the
32 | // open transaction. This prevents the confirming signer from
33 | // withholding signatures for the declaration and closing transactions.
34 | extraSignerStrs := [2]string{}
35 | {
36 | extraSigner, err := strkey.NewSignedPayload(p.ConfirmingSigner.Address(), p.DeclarationTxHash[:])
37 | if err != nil {
38 | return nil, err
39 | }
40 | extraSignerStrs[0], err = extraSigner.Encode()
41 | if err != nil {
42 | return nil, err
43 | }
44 | }
45 | {
46 | extraSigner, err := strkey.NewSignedPayload(p.ConfirmingSigner.Address(), p.CloseTxHash[:])
47 | if err != nil {
48 | return nil, err
49 | }
50 | extraSignerStrs[1], err = extraSigner.Encode()
51 | if err != nil {
52 | return nil, err
53 | }
54 | }
55 |
56 | tp := txnbuild.TransactionParams{
57 | SourceAccount: &txnbuild.SimpleAccount{
58 | AccountID: p.InitiatorChannelAccount.Address(),
59 | Sequence: p.StartSequence,
60 | },
61 | BaseFee: 0,
62 | Preconditions: txnbuild.Preconditions{
63 | TimeBounds: txnbuild.NewTimebounds(0, p.ExpiresAt.UTC().Unix()),
64 | ExtraSigners: extraSignerStrs[:],
65 | },
66 | }
67 |
68 | // I sponsoring ledger entries on EI
69 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.InitiatorSigner.Address(), SponsoredID: p.InitiatorChannelAccount.Address()})
70 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{
71 | SourceAccount: p.InitiatorChannelAccount.Address(),
72 | MasterWeight: txnbuild.NewThreshold(0),
73 | LowThreshold: txnbuild.NewThreshold(2),
74 | MediumThreshold: txnbuild.NewThreshold(2),
75 | HighThreshold: txnbuild.NewThreshold(2),
76 | Signer: &txnbuild.Signer{Address: p.InitiatorSigner.Address(), Weight: 1},
77 | })
78 | if !p.Asset.IsNative() {
79 | tp.Operations = append(tp.Operations, &txnbuild.ChangeTrust{
80 | Line: p.Asset.MustToChangeTrustAsset(),
81 | Limit: amount.StringFromInt64(math.MaxInt64),
82 | SourceAccount: p.InitiatorChannelAccount.Address(),
83 | })
84 | }
85 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.InitiatorChannelAccount.Address()})
86 |
87 | // I sponsoring ledger entries on ER
88 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.InitiatorSigner.Address(), SponsoredID: p.ResponderChannelAccount.Address()})
89 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{
90 | SourceAccount: p.ResponderChannelAccount.Address(),
91 | Signer: &txnbuild.Signer{Address: p.InitiatorSigner.Address(), Weight: 1},
92 | })
93 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.ResponderChannelAccount.Address()})
94 |
95 | // R sponsoring ledger entries on ER
96 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.ResponderSigner.Address(), SponsoredID: p.ResponderChannelAccount.Address()})
97 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{
98 | SourceAccount: p.ResponderChannelAccount.Address(),
99 | MasterWeight: txnbuild.NewThreshold(0),
100 | LowThreshold: txnbuild.NewThreshold(2),
101 | MediumThreshold: txnbuild.NewThreshold(2),
102 | HighThreshold: txnbuild.NewThreshold(2),
103 | Signer: &txnbuild.Signer{Address: p.ResponderSigner.Address(), Weight: 1},
104 | })
105 | if !p.Asset.IsNative() {
106 | tp.Operations = append(tp.Operations, &txnbuild.ChangeTrust{
107 | Line: p.Asset.MustToChangeTrustAsset(),
108 | Limit: amount.StringFromInt64(math.MaxInt64),
109 | SourceAccount: p.ResponderChannelAccount.Address(),
110 | })
111 | }
112 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.ResponderChannelAccount.Address()})
113 |
114 | // R sponsoring ledger entries on EI
115 | tp.Operations = append(tp.Operations, &txnbuild.BeginSponsoringFutureReserves{SourceAccount: p.ResponderSigner.Address(), SponsoredID: p.InitiatorChannelAccount.Address()})
116 | tp.Operations = append(tp.Operations, &txnbuild.SetOptions{
117 | SourceAccount: p.InitiatorChannelAccount.Address(),
118 | Signer: &txnbuild.Signer{Address: p.ResponderSigner.Address(), Weight: 1},
119 | })
120 | tp.Operations = append(tp.Operations, &txnbuild.EndSponsoringFutureReserves{SourceAccount: p.InitiatorChannelAccount.Address()})
121 |
122 | tx, err := txnbuild.NewTransaction(tp)
123 | if err != nil {
124 | return nil, err
125 | }
126 | return tx, nil
127 | }
128 |
--------------------------------------------------------------------------------
/sdk/txbuild/sequence.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import "fmt"
4 |
5 | const m = 2
6 |
7 | func startSequenceOfIteration(startSequence int64, iterationNumber int64) int64 {
8 | return startSequence + iterationNumber*m
9 | }
10 |
11 | type TransactionType string
12 |
13 | const (
14 | TransactionTypeUnrecognized TransactionType = "unrecognized"
15 | TransactionTypeOpen TransactionType = "open"
16 | TransactionTypeDeclaration TransactionType = "declaration"
17 | TransactionTypeClose TransactionType = "close"
18 | )
19 |
20 | func SequenceNumberToTransactionType(startingSeqNum, seqNum int64) TransactionType {
21 | seqRelative := seqNum - startingSeqNum
22 | if seqRelative == 0 {
23 | return TransactionTypeOpen
24 | } else if seqRelative > 0 && seqRelative < m {
25 | return TransactionTypeUnrecognized
26 | } else if seqRelative%m == 0 {
27 | return TransactionTypeDeclaration
28 | } else if seqRelative%m == 1 {
29 | return TransactionTypeClose
30 | }
31 | panic(fmt.Errorf("unhandled sequence number: startingSeqNum=%d seqNum=%d", startingSeqNum, seqNum))
32 | }
33 |
--------------------------------------------------------------------------------
/sdk/txbuild/sequence_test.go:
--------------------------------------------------------------------------------
1 | package txbuild
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestSequenceNumberToTransactionType(t *testing.T) {
10 | assert.Equal(t, TransactionTypeOpen, SequenceNumberToTransactionType(100, 100))
11 | assert.Equal(t, TransactionTypeOpen, SequenceNumberToTransactionType(101, 101))
12 |
13 | assert.Equal(t, TransactionTypeUnrecognized, SequenceNumberToTransactionType(100, 101))
14 | assert.Equal(t, TransactionTypeUnrecognized, SequenceNumberToTransactionType(101, 102))
15 |
16 | assert.Equal(t, TransactionTypeDeclaration, SequenceNumberToTransactionType(101, 103))
17 | assert.Equal(t, TransactionTypeClose, SequenceNumberToTransactionType(101, 104))
18 |
19 | assert.Equal(t, TransactionTypeDeclaration, SequenceNumberToTransactionType(100, 102))
20 | assert.Equal(t, TransactionTypeClose, SequenceNumberToTransactionType(100, 103))
21 | }
22 |
--------------------------------------------------------------------------------
/sdk/txbuild/txbuildtest/txbuildtest.go:
--------------------------------------------------------------------------------
1 | package txbuildtest
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "time"
7 |
8 | "github.com/stellar/go/txnbuild"
9 | "github.com/stellar/go/xdr"
10 | )
11 |
12 | // BuildResult returns a result XDR base64 encoded that is successful or not
13 | // based on the input parameter.
14 | func BuildResultXDR(success bool) (string, error) {
15 | var code xdr.TransactionResultCode
16 | if success {
17 | code = xdr.TransactionResultCodeTxSuccess
18 | } else {
19 | code = xdr.TransactionResultCodeTxFailed
20 | }
21 | tr := xdr.TransactionResult{
22 | FeeCharged: 123,
23 | Result: xdr.TransactionResultResult{
24 | Code: code,
25 | Results: &[]xdr.OperationResult{},
26 | },
27 | }
28 | trXDR, err := xdr.MarshalBase64(tr)
29 | if err != nil {
30 | return "", fmt.Errorf("encoding transaction result to base64 xdr: %w", err)
31 | }
32 | return trXDR, nil
33 | }
34 |
35 | // BuildResultMetaXDR returns a result meta XDR base64 encoded that contains
36 | // the input ledger entry changes. Only creates one operation meta for
37 | // simiplicity.
38 | func BuildResultMetaXDR(ledgerEntryResults []xdr.LedgerEntryData) (string, error) {
39 | tm := xdr.TransactionMeta{
40 | V: 2,
41 | V2: &xdr.TransactionMetaV2{
42 | Operations: []xdr.OperationMeta{
43 | {},
44 | },
45 | },
46 | }
47 |
48 | rand := rand.New(rand.NewSource(time.Now().UnixNano()))
49 | for _, result := range ledgerEntryResults {
50 | change := xdr.LedgerEntryChange{}
51 | // When operations like ChangeTrustOp execute they potentially create or
52 | // update existing ledger entries. The operation encoded in the
53 | // transaction doesn't actually know which will occur because it depends
54 | // on the state of the network at the time the transaction is executed.
55 | // Randomly simulating whether a create or update change occurs
56 | // simulates that uncertainty while also ensuring we broadly test that
57 | // different behavior across all of our tests.
58 | if rand.Int()%2 == 0 {
59 | change.Type = xdr.LedgerEntryChangeTypeLedgerEntryCreated
60 | change.Created = &xdr.LedgerEntry{Data: result}
61 | } else {
62 | change.Type = xdr.LedgerEntryChangeTypeLedgerEntryUpdated
63 | change.Updated = &xdr.LedgerEntry{Data: result}
64 | }
65 | tm.V2.Operations[0].Changes = append(tm.V2.Operations[0].Changes, change)
66 | }
67 |
68 | tmXDR, err := xdr.MarshalBase64(tm)
69 | if err != nil {
70 | return "", fmt.Errorf("encoding transaction meta to base64 xdr: %w", err)
71 | }
72 | return tmXDR, nil
73 | }
74 |
75 | type OpenResultMetaParams struct {
76 | InitiatorSigner string
77 | InitiatorSignerWeight uint32 // Defaults to 1 if not set.
78 | ResponderSigner string
79 | ResponderSignerWeight uint32 // Defaults to 1 if not set.
80 | ExtraSigner string
81 | InitiatorChannelAccount string
82 | ResponderChannelAccount string
83 | Thresholds xdr.Thresholds // Defaults to 0, 2, 2, 2 if not set.
84 | StartSequence int64
85 | Asset txnbuild.Asset
86 | TrustLineFlag xdr.TrustLineFlags // Defaults to authorized flag.
87 | }
88 |
89 | func BuildOpenResultMetaXDR(params OpenResultMetaParams) (string, error) {
90 | initiatorSignerWeight := uint32(1)
91 | if params.InitiatorSignerWeight > 0 {
92 | initiatorSignerWeight = params.InitiatorSignerWeight
93 | }
94 | responderSignerWeight := uint32(1)
95 | if params.ResponderSignerWeight > 0 {
96 | responderSignerWeight = params.ResponderSignerWeight
97 | }
98 | thresholds := xdr.Thresholds{0, 2, 2, 2}
99 | if params.Thresholds != (xdr.Thresholds{}) {
100 | thresholds = params.Thresholds
101 | }
102 | led := []xdr.LedgerEntryData{
103 | {
104 | Type: xdr.LedgerEntryTypeAccount,
105 | Account: &xdr.AccountEntry{
106 | AccountId: xdr.MustAddress(params.InitiatorChannelAccount),
107 | SeqNum: xdr.SequenceNumber(params.StartSequence),
108 | Signers: []xdr.Signer{
109 | {
110 | Key: xdr.MustSigner(params.InitiatorSigner),
111 | Weight: xdr.Uint32(initiatorSignerWeight),
112 | },
113 | {
114 | Key: xdr.MustSigner(params.ResponderSigner),
115 | Weight: xdr.Uint32(responderSignerWeight),
116 | },
117 | },
118 | Thresholds: thresholds,
119 | },
120 | },
121 | {
122 | Type: xdr.LedgerEntryTypeAccount,
123 | Account: &xdr.AccountEntry{
124 | AccountId: xdr.MustAddress(params.ResponderChannelAccount),
125 | SeqNum: xdr.SequenceNumber(1),
126 | Signers: []xdr.Signer{
127 | {
128 | Key: xdr.MustSigner(params.InitiatorSigner),
129 | Weight: xdr.Uint32(initiatorSignerWeight),
130 | },
131 | {
132 | Key: xdr.MustSigner(params.ResponderSigner),
133 | Weight: xdr.Uint32(responderSignerWeight),
134 | },
135 | },
136 | Thresholds: thresholds,
137 | },
138 | },
139 | }
140 | if params.ExtraSigner != "" {
141 | led[0].Account.Signers = append(led[0].Account.Signers, xdr.Signer{
142 | Key: xdr.MustSigner(params.ExtraSigner),
143 | Weight: 1,
144 | })
145 | led[1].Account.Signers = append(led[0].Account.Signers, xdr.Signer{
146 | Key: xdr.MustSigner(params.ExtraSigner),
147 | Weight: 1,
148 | })
149 | }
150 |
151 | if !params.Asset.IsNative() {
152 | trustlineFlag := xdr.TrustLineFlagsAuthorizedFlag
153 | if params.TrustLineFlag != 0 {
154 | trustlineFlag = params.TrustLineFlag
155 | }
156 | led = append(led, []xdr.LedgerEntryData{
157 | {
158 | Type: xdr.LedgerEntryTypeTrustline,
159 | TrustLine: &xdr.TrustLineEntry{
160 | AccountId: xdr.MustAddress(params.InitiatorChannelAccount),
161 | Balance: 0,
162 | Asset: xdr.MustNewCreditAsset(params.Asset.GetCode(), params.Asset.GetIssuer()).ToTrustLineAsset(),
163 | Flags: xdr.Uint32(trustlineFlag),
164 | },
165 | },
166 | {
167 | Type: xdr.LedgerEntryTypeTrustline,
168 | TrustLine: &xdr.TrustLineEntry{
169 | AccountId: xdr.MustAddress(params.ResponderChannelAccount),
170 | Balance: 0,
171 | Asset: xdr.MustNewCreditAsset(params.Asset.GetCode(), params.Asset.GetIssuer()).ToTrustLineAsset(),
172 | Flags: xdr.Uint32(trustlineFlag),
173 | },
174 | },
175 | }...)
176 | }
177 |
178 | return BuildResultMetaXDR(led)
179 | }
180 |
--------------------------------------------------------------------------------
/sdk/txbuild/txbuildtest/txbuildtest_test.go:
--------------------------------------------------------------------------------
1 | package txbuildtest
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stellar/go/xdr"
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func Test_txbuildtest_buildResultXDR(t *testing.T) {
12 | r, err := BuildResultXDR(true)
13 | require.NoError(t, err)
14 |
15 | var txResult xdr.TransactionResult
16 | err = xdr.SafeUnmarshalBase64(r, &txResult)
17 | require.NoError(t, err)
18 | assert.True(t, txResult.Successful())
19 |
20 | r, err = BuildResultXDR(false)
21 | require.NoError(t, err)
22 |
23 | err = xdr.SafeUnmarshalBase64(r, &txResult)
24 | require.NoError(t, err)
25 | assert.False(t, txResult.Successful())
26 | }
27 |
28 | func Test_txbuildtest_buildResultMetaXDR(t *testing.T) {
29 | led := []xdr.LedgerEntryData{
30 | {
31 | Type: xdr.LedgerEntryTypeAccount,
32 | Account: &xdr.AccountEntry{
33 | AccountId: xdr.MustAddress("GAKDNXUGEIRGESAXOPUHU4GOWLVYGQFJVHQOGFXKBXDGZ7AKMPPSDDPV"),
34 | },
35 | },
36 | {
37 | Type: xdr.LedgerEntryTypeTrustline,
38 | TrustLine: &xdr.TrustLineEntry{
39 | AccountId: xdr.MustAddress("GAKDNXUGEIRGESAXOPUHU4GOWLVYGQFJVHQOGFXKBXDGZ7AKMPPSDDPV"),
40 | Balance: xdr.Int64(100),
41 | },
42 | },
43 | }
44 |
45 | m, err := BuildResultMetaXDR(led)
46 | require.NoError(t, err)
47 |
48 | // Validate the ledger entry changes are correct.
49 | var txMeta xdr.TransactionMeta
50 | err = xdr.SafeUnmarshalBase64(m, &txMeta)
51 | require.NoError(t, err)
52 |
53 | txMetaV2, ok := txMeta.GetV2()
54 | require.True(t, ok)
55 |
56 | for _, o := range txMetaV2.Operations {
57 | for i, change := range o.Changes {
58 | created, createdOK := change.GetCreated()
59 | updated, updatedOK := change.GetUpdated()
60 | if createdOK {
61 | assert.Equal(t, led[i], created.Data)
62 | } else if updatedOK {
63 | assert.Equal(t, led[i], updated.Data)
64 | } else {
65 | assert.Fail(t, "change didn't contain a created or updated")
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/specifications/diagrams/README.md:
--------------------------------------------------------------------------------
1 | # Payment Channel Diagrams
2 |
3 | ## How to view
4 | These diagrams were created using the [mermaid diagram live editor](https://mermaid-js.github.io/mermaid-live-editor) which uses this [syntax](https://mermaid-js.github.io/mermaid/#/sequenceDiagram). To view the diagrams below copy the text into the `code` section of the live editor.
5 |
6 | ### Sequence Diagrams
7 | [setup-payment-close.txt](./sequence-diagrams/setup-payment-close.txt)
8 | - walks through the steps of the basic case for setting up a channel, updating it, and closing it either cooperatively or non-cooperatively
9 |
10 | [withdraw.txt](./sequence-diagrams/withdraw.txt)
11 | - a withdrawal by I after setup and a single payment
12 |
13 | ### State Diagrams
14 | [channel-state.txt](./state-diagrams/channel-state.txt)
15 | - state diagram describing the possible channel states and actions that trigger movement to next states
16 |
--------------------------------------------------------------------------------
/specifications/diagrams/sequence-diagrams/setup-payment-close.txt:
--------------------------------------------------------------------------------
1 | sequenceDiagram
2 | participant I as Initiator
3 | participant R as Responder
4 |
5 | Note over I,R: 1. Setup
6 | Note right of R: i = 0, e = 0
7 | I-->>I: create and deposit funds to MI
8 | R-->>R: create and deposits funds to MR
9 | I->>R: Create open transaction F
10 | I->>I: increment i
11 | R->>R: increment i
12 | Note right of R: i = 1
13 | I->>R: Create and sign C_1
14 | R-->>I: sign C_1, send back
15 | I->>R: Create and sign D_1
16 | R-->>I: sign D_1, send back
17 | I->>R: Sign F
18 | R->>R: Sign F, submit
19 |
20 | Note over I,R: 2. Payment
21 | I->>I: increment i
22 | R->>R: increment i
23 | Note right of R: i=2
24 | I->>R: Create and sign C_2
25 | R-->>I: Sign C_2, send back
26 | I->>R: Create and sign D_2
27 | R-->>I: Sign D-2, send back
28 |
29 | Note over I,R: 3. Closing
30 | Note right of R: e = 2
31 | alt Coordinated Close
32 | I->>I: submit D_2
33 | I->>R: modify C_2 to be able to submit immediately, sign
34 | R->>R: re-sign C_2, submit
35 | else Uncoordinated Close, non-responsive actor
36 | I->>I: submit D_2
37 | I->>R: modify C_2 to be able to submit immediately, sign
38 | Note over R: R is non-responsive
39 | I->>I: wait observation period O
40 | I->>I: submit C_2
41 | else Uncoordinated Close, malicious actor
42 | I->>I: submit D_1
43 | Note over R: R notices a close from an expired payment has been initiated
44 | R->>R: submit D_2, wait observation period O to pass
45 | R->>R: submit C_2
46 | end
--------------------------------------------------------------------------------
/specifications/diagrams/sequence-diagrams/withdraw.txt:
--------------------------------------------------------------------------------
1 | sequenceDiagram
2 | participant I as Initiator
3 | participant R as Responder
4 |
5 | Note over I,R: 1. Setup and a single payment
6 | Note right of R: i = 2, e = 0
7 | Note left of I: MI.seqNum = 1000
8 |
9 | Note over I,R: 2. Withdrawal by I
10 | I->>I: increment i
11 | R->>R: increment i
12 | Note right of R: i = 3
13 | I->>R: create W_3, which has a payment op from MI to I, and bumps MI.seqNum to s_3
14 | I->>I: set e' = e, set e = i, then increment i
15 | R->>R: set e' = e, set e = i, then increment i
16 | Note right of R: e' = 0, e = 3, i = 4
17 | I->>R: create and sign C_4, which has the last agreed upon payouts minus I's payout from W_3
18 | R-->>I: sign C_4, send back
19 | I->>R: create and sign D_4
20 | R-->>I: sign D_4, send back
21 | R->>I: sign W_4
22 | I->>I: submit W_4
23 |
24 | alt W_4 successful
25 | Note right of R: withdrawal complete
26 | Note left of I: MI.seqNum = 1007
27 | else W_4 unsuccessful
28 | I->>I: set e = e'
29 | R->>R: set e = e'
30 | Note right of R: e = 0
31 | Note right of R: note: D_4.minSeq = s_3, with an unsuccessful W_3, D_4 is impossible to submit successfully
32 | end
33 |
--------------------------------------------------------------------------------
/specifications/diagrams/state-diagrams/channel-proposal.txt:
--------------------------------------------------------------------------------
1 | stateDiagram-v2
2 | [*] --> Idle
3 | note left of [*]: Initiator proposing a channel Step1 agree on C1/D1 Step2 agree on F
4 |
5 | Idle --> Connecting: Connect()
6 | Connecting --> Listening: handshake successful
7 | Connecting --> Idle: handshake failed
8 |
9 | %% Proposing:
10 | Listening --> Proposing: Propose() 1. Create D_1, C_1 Sign C_1 Send both to R 2. Create F, sign F, send
11 | Proposing --> ResponseReceived
12 | note right of Proposing: waiting/listening while in proposing state
13 | Proposing --> NoResponseReceived: timeout expires
14 |
15 | NoResponseReceived --> Listening
16 | ResponseReceived --> Listening: Response invalid, or rejected
17 | ResponseReceived --> ProposalRejected: rejected, timeout error, invalid response
18 | ProposalRejected --> Listening
19 | ResponseReceived --> ProposalAccepted: valid Response 1. receieved signed C_1/D_1 2. received signed F
20 | ProposalAccepted --> Listening: 1. sign D_1 and send both back 2. submit F
21 |
--------------------------------------------------------------------------------
/specifications/diagrams/state-diagrams/channel-state.txt:
--------------------------------------------------------------------------------
1 | stateDiagram-v2
2 | [*] --> Waiting: NewChannel()
3 | note left of [*]: Initiator opening a channel
4 |
5 | Waiting --> Active: channel.Open()
6 |
7 | Active --> Closing: channel.CloseStart()
8 |
9 | Closing --> Closed: Responder calls channel.CloseCoordinated()
10 | Closing --> Closed: channel.CloseUnCoordinated()
11 |
12 | Closed --> [*]
13 |
--------------------------------------------------------------------------------
/specifications/diagrams/state-diagrams/payment-proposal-threads.txt:
--------------------------------------------------------------------------------
1 | stateDiagram-v2
2 | [*] --> Idle
3 | note left of [*]: Responder responding to payment
4 | note left of Idle: foreground task
5 | Idle --> Connecting: Connect()
6 | Connecting --> Ready: handshake successful
7 | Connecting --> Idle: handshake failed
8 |
9 | [*] --> Listening
10 | note left of Listening: background task
11 | Listening --> NewThread: received message
12 | state NewThread{
13 | [*] --> ProcessingReceivedMessage
14 | ProcessingReceivedMessage --> StorePaymentProposal
15 | StorePaymentProposal --> [*]
16 | }
17 |
18 | Ready --> ConfirmingPayment: FindPaymentProposal()
19 | ConfirmingPayment --> Ready: ConfirmPayment()
20 | ConfirmingPayment --> Ready: DeclinePayment()
21 |
--------------------------------------------------------------------------------