Skip to content

Commit 3d4243c

Browse files
authored
feat: extract receipt from ucan/conclude invocation (#144)
The receipts endpoint returns an agent message that contains the receipt. However the agent message may have been a `ucan/conclude` invocation with the receipt attached. This PR enables the receipts client to extract the receipt from the agent message in this case. Port of https://github.com/storacha/upload-service/blob/e41f9293fe25927ace7a69522d1a5593aa61603c/packages/upload-client/src/receipts.js#L138-L167
1 parent e32327d commit 3d4243c

File tree

4 files changed

+174
-3
lines changed

4 files changed

+174
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ require (
1515
github.com/multiformats/go-varint v0.1.0
1616
github.com/spf13/afero v1.6.0
1717
github.com/storacha/go-libstoracha v0.2.8
18-
github.com/storacha/go-ucanto v0.6.2
18+
github.com/storacha/go-ucanto v0.6.7
1919
github.com/stretchr/testify v1.10.0
2020
github.com/urfave/cli/v2 v2.25.7 // indirect
2121
golang.org/x/sync v0.14.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,8 +550,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
550550
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
551551
github.com/storacha/go-libstoracha v0.2.8 h1:vQ+kCnRruzAekAiW4nJ0JnQrH4NbareSEydX+Laviv0=
552552
github.com/storacha/go-libstoracha v0.2.8/go.mod h1:zzeqIZhBBuWR2dkGygYqv4Bhg3JsvHuuvDCcdXCwGhg=
553-
github.com/storacha/go-ucanto v0.6.2 h1:4049ZUXGsdwdu9+FU6CssMDmpxQJbHTf6+EwEinaXmU=
554-
github.com/storacha/go-ucanto v0.6.2/go.mod h1:O35Ze4x18EWtz3ftRXXd/mTZ+b8OQVjYYrnadJ/xNjg=
553+
github.com/storacha/go-ucanto v0.6.7 h1:rKNCyt9n4FwzCXIb+FDQ2Lcic+yNQK10PkGi4VipoM0=
554+
github.com/storacha/go-ucanto v0.6.7/go.mod h1:O35Ze4x18EWtz3ftRXXd/mTZ+b8OQVjYYrnadJ/xNjg=
555555
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
556556
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
557557
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

pkg/receipt/client.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"iter"
78
"net/http"
89
"net/url"
910
"time"
1011

1112
"github.com/storacha/go-libstoracha/capabilities/types"
13+
ucancap "github.com/storacha/go-libstoracha/capabilities/ucan"
14+
"github.com/storacha/go-ucanto/core/invocation"
15+
"github.com/storacha/go-ucanto/core/ipld"
1216
"github.com/storacha/go-ucanto/core/message"
1317
"github.com/storacha/go-ucanto/core/receipt"
1418
"github.com/storacha/go-ucanto/transport"
1519
"github.com/storacha/go-ucanto/transport/car"
1620
ucanhttp "github.com/storacha/go-ucanto/transport/http"
1721
"github.com/storacha/go-ucanto/ucan"
22+
"github.com/storacha/go-ucanto/validator"
1823
)
1924

2025
var ErrNotFound = errors.New("receipt not found")
@@ -88,13 +93,63 @@ func (c *Client) Fetch(ctx context.Context, task ucan.Link) (receipt.AnyReceipt,
8893

8994
rcptlnk, ok := msg.Get(task)
9095
if !ok {
96+
// This could be an agent message that contains a ucan/conclude invocation
97+
// that contains the receipt.
98+
for _, root := range msg.Invocations() {
99+
inv, ok, err := msg.Invocation(root)
100+
if err != nil || !ok || len(inv.Capabilities()) != 1 {
101+
continue
102+
}
103+
rcpt, err := extractConcludeReceipt(task, inv, msg.Blocks())
104+
if err != nil {
105+
continue
106+
}
107+
return rcpt, nil
108+
}
109+
// This could be an agent message that contains a receipt for a
110+
// ucan/conclude invocation that contains the receipt.
111+
for _, root := range msg.Receipts() {
112+
concludeRcpt, ok, err := msg.Receipt(root)
113+
if err != nil || !ok {
114+
continue
115+
}
116+
inv, ok := concludeRcpt.Ran().Invocation()
117+
if !ok {
118+
continue
119+
}
120+
rcpt, err := extractConcludeReceipt(task, inv, msg.Blocks())
121+
if err != nil {
122+
continue
123+
}
124+
return rcpt, nil
125+
}
91126
return nil, errors.New("receipt not found in agent message")
92127
}
93128

94129
reader := receipt.NewAnyReceiptReader(types.Converters...)
95130
return reader.Read(rcptlnk, msg.Blocks())
96131
}
97132

133+
// extractConcludeReceipt extracts the receipt for the passed task from the
134+
// passed invocation, if it is an invocation of `ucan/conclude`.
135+
func extractConcludeReceipt(task ipld.Link, inv invocation.Invocation, blocks iter.Seq2[ipld.Block, error]) (receipt.AnyReceipt, error) {
136+
var err error
137+
cap := inv.Capabilities()[0]
138+
match, err := ucancap.Conclude.Match(validator.NewSource(cap, inv))
139+
if err != nil {
140+
return nil, err
141+
}
142+
reader := receipt.NewAnyReceiptReader(types.Converters...)
143+
rcpt, err := reader.Read(match.Value().Nb().Receipt, blocks)
144+
if err != nil {
145+
return nil, err
146+
}
147+
if rcpt.Ran().Link().String() != task.String() {
148+
return nil, fmt.Errorf("receipt is not for task: %s", task)
149+
}
150+
return rcpt, nil
151+
}
152+
98153
type pollConfig struct {
99154
interval *time.Duration
100155
retries *int

pkg/receipt/client_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010
"time"
1111

12+
ucancap "github.com/storacha/go-libstoracha/capabilities/ucan"
1213
"github.com/storacha/go-libstoracha/testutil"
1314
"github.com/storacha/go-ucanto/core/invocation"
1415
"github.com/storacha/go-ucanto/core/message"
@@ -62,6 +63,121 @@ func TestFetch(t *testing.T) {
6263
require.Equal(t, inv.Link(), result.Ran().Link())
6364
})
6465

66+
t.Run("found in ucan/conclude invocation", func(t *testing.T) {
67+
inv, err := invocation.Invoke(
68+
testutil.Alice,
69+
testutil.Service,
70+
ucan.NewCapability(
71+
"test/receipt",
72+
testutil.Alice.DID().String(),
73+
ucan.NoCaveats{},
74+
),
75+
)
76+
require.NoError(t, err)
77+
78+
rcpt, err := receipt.Issue(
79+
testutil.Alice,
80+
result.Ok[ok.Unit, failure.IPLDBuilderFailure](ok.Unit{}),
81+
ran.FromInvocation(inv),
82+
)
83+
require.NoError(t, err)
84+
85+
ccInv, err := ucancap.Conclude.Invoke(
86+
testutil.Alice,
87+
testutil.Service,
88+
testutil.Alice.DID().String(),
89+
ucancap.ConcludeCaveats{
90+
Receipt: rcpt.Root().Link(),
91+
},
92+
)
93+
require.NoError(t, err)
94+
95+
// attach the receipt to the conclude invocation
96+
for b, err := range rcpt.Blocks() {
97+
require.NoError(t, err)
98+
require.NoError(t, ccInv.Attach(b))
99+
}
100+
101+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102+
msg, err := message.Build([]invocation.Invocation{ccInv}, nil)
103+
require.NoError(t, err)
104+
res, err := response.Encode(msg)
105+
require.NoError(t, err)
106+
_, err = io.Copy(w, res.Body())
107+
require.NoError(t, err)
108+
}))
109+
defer server.Close()
110+
111+
endpoint, err := url.Parse(server.URL)
112+
require.NoError(t, err)
113+
114+
client := receiptclient.New(endpoint)
115+
result, err := client.Fetch(t.Context(), inv.Link())
116+
require.NoError(t, err)
117+
require.Equal(t, inv.Link(), result.Ran().Link())
118+
})
119+
120+
t.Run("found in ucan/conclude receipt", func(t *testing.T) {
121+
inv, err := invocation.Invoke(
122+
testutil.Alice,
123+
testutil.Service,
124+
ucan.NewCapability(
125+
"test/receipt",
126+
testutil.Alice.DID().String(),
127+
ucan.NoCaveats{},
128+
),
129+
)
130+
require.NoError(t, err)
131+
132+
rcpt, err := receipt.Issue(
133+
testutil.Alice,
134+
result.Ok[ok.Unit, failure.IPLDBuilderFailure](ok.Unit{}),
135+
ran.FromInvocation(inv),
136+
)
137+
require.NoError(t, err)
138+
139+
ccInv, err := ucancap.Conclude.Invoke(
140+
testutil.Alice,
141+
testutil.Service,
142+
testutil.Alice.DID().String(),
143+
ucancap.ConcludeCaveats{
144+
Receipt: rcpt.Root().Link(),
145+
},
146+
)
147+
require.NoError(t, err)
148+
149+
// attach the receipt to the conclude invocation
150+
for b, err := range rcpt.Blocks() {
151+
require.NoError(t, err)
152+
require.NoError(t, ccInv.Attach(b))
153+
}
154+
155+
ccRcpt, err := receipt.Issue(
156+
testutil.Service,
157+
result.Ok[ok.Unit, failure.IPLDBuilderFailure](ok.Unit{}),
158+
ran.FromInvocation(ccInv),
159+
)
160+
require.NoError(t, err)
161+
162+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
163+
msg, err := message.Build(nil, []receipt.AnyReceipt{ccRcpt})
164+
require.NoError(t, err)
165+
res, err := response.Encode(msg)
166+
require.NoError(t, err)
167+
_, err = io.Copy(w, res.Body())
168+
require.NoError(t, err)
169+
}))
170+
defer server.Close()
171+
172+
endpoint, err := url.Parse(server.URL)
173+
require.NoError(t, err)
174+
175+
client := receiptclient.New(endpoint)
176+
result, err := client.Fetch(t.Context(), inv.Link())
177+
require.NoError(t, err)
178+
require.Equal(t, inv.Link(), result.Ran().Link())
179+
})
180+
65181
t.Run("not found", func(t *testing.T) {
66182
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67183
w.WriteHeader(http.StatusNotFound)

0 commit comments

Comments
 (0)