forked from joe-p/arc58
-
Notifications
You must be signed in to change notification settings - Fork 0
/
abstract_account_plugins.test.ts
270 lines (237 loc) · 9.32 KB
/
abstract_account_plugins.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import { describe, test, beforeAll, beforeEach, expect } from '@jest/globals';
import { algorandFixture } from '@algorandfoundation/algokit-utils/testing';
import * as algokit from '@algorandfoundation/algokit-utils';
import algosdk from 'algosdk';
import { AbstractedAccountClient } from '../contracts/clients/AbstractedAccountClient';
import { SubscriptionPluginClient } from '../contracts/clients/SubscriptionPluginClient';
import { OptInPluginClient } from '../contracts/clients/OptInPluginClient';
const ZERO_ADDRESS = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ';
const fixture = algorandFixture();
describe('Abstracted Subscription Program', () => {
/** Alice's externally owned account (ie. a keypair account she has in Pera) */
let aliceEOA: algosdk.Account;
/** The address of Alice's new abstracted account. Sends app calls from aliceEOA unless otherwise specified */
let aliceAbstractedAccount: string;
/** The client for Alice's abstracted account */
let abstractedAccountClient: AbstractedAccountClient;
/** The client for the subscription plugin */
let subPluginClient: SubscriptionPluginClient;
/** The ID of the subscription plugin */
let subPluginID: number;
/** The client for the opt-in plugin */
let optInPluginClient: OptInPluginClient;
/** The ID of the opt-in plugin */
let optInPluginID: number;
/** The suggested params for transactions */
let suggestedParams: algosdk.SuggestedParams;
/** The maximum uint64 value. Used to indicate a never-expiring plugin */
const maxUint64 = BigInt('18446744073709551615');
beforeEach(fixture.beforeEach);
beforeAll(async () => {
await fixture.beforeEach();
const { algod, testAccount } = fixture.context;
suggestedParams = await algod.getTransactionParams().do();
aliceEOA = testAccount;
abstractedAccountClient = new AbstractedAccountClient(
{
sender: aliceEOA,
resolveBy: 'id',
id: 0,
},
algod
);
// Create an abstracted account app
await abstractedAccountClient.create.createApplication({
// Set address to ZERO_ADDRESS so the app address is used
controlledAddress: ZERO_ADDRESS,
// aliceEOA will be the admin
admin: aliceEOA.addr,
});
aliceAbstractedAccount = (await abstractedAccountClient.appClient.getAppReference()).appAddress;
// Fund the abstracted account with 0.2 ALGO so it can hold an ASA
await abstractedAccountClient.appClient.fundAppAccount({ amount: algokit.microAlgos(200_000) });
// Deploy the subscription plugin
subPluginClient = new SubscriptionPluginClient(
{
sender: aliceEOA,
resolveBy: 'id',
id: 0,
},
algod
);
await subPluginClient.create.createApplication({});
subPluginID = Number((await subPluginClient.appClient.getAppReference()).appId);
// Deploy the opt-in plugin
optInPluginClient = new OptInPluginClient(
{
sender: aliceEOA,
resolveBy: 'id',
id: 0,
},
algod
);
await optInPluginClient.create.createApplication({});
optInPluginID = Number((await optInPluginClient.appClient.getAppReference()).appId);
});
describe('Unnamed Subscription Plugin', () => {
/** Another account that the subscription payments will go to */
const joe = '46XYR7OTRZXISI2TRSBDWPUVQT4ECBWNI7TFWPPS6EKAPJ7W5OBXSNG66M';
/** The box key for the subscription plugin */
let pluginBox: Uint8Array;
/** The boxes to pass to app calls */
let boxes: Uint8Array[];
beforeAll(() => {
/** The box key for a plugin is `p + plugin ID + allowed caller` */
pluginBox = new Uint8Array(
Buffer.concat([
Buffer.from('p'),
Buffer.from(algosdk.encodeUint64(subPluginID)),
algosdk.decodeAddress(ZERO_ADDRESS).publicKey,
])
);
boxes = [pluginBox];
});
test('Alice adds the app to the abstracted account', async () => {
await abstractedAccountClient.appClient.fundAppAccount({ amount: algokit.microAlgos(22100) });
await abstractedAccountClient.arc58AddPlugin(
{
// Add the subscription plugin
app: subPluginID,
// Set address to ZERO_ADDRESS so anyone can call it
allowedCaller: ZERO_ADDRESS,
// Set end to maxUint64 so it never expires
end: maxUint64,
},
{ boxes }
);
});
test('Someone calls the program to trigger payment', async () => {
const { algod, testAccount } = fixture.context;
boxes = [
new Uint8Array(
Buffer.concat([
Buffer.from('p'),
Buffer.from(algosdk.encodeUint64(subPluginID)),
algosdk.decodeAddress(ZERO_ADDRESS).publicKey,
])
),
];
const alicePreBalance = await algod.accountInformation(aliceAbstractedAccount).do();
const joePreBalance = await algod.accountInformation(joe).do();
// Get the call to the subscription plugin
const makePaymentTxn = (
await subPluginClient
.compose()
.makePayment(
// Send a payment from the abstracted account to Joe
{ sender: aliceAbstractedAccount, _acctRef: joe },
// Double the fee to cover the inner txn fee
{ sender: testAccount, sendParams: { fee: algokit.microAlgos(2_000) } }
)
.atc()
).buildGroup()[0].txn;
// Compose the group needed to actually use the plugin
await abstractedAccountClient
.compose()
// Step one: rekey to the plugin
.arc58RekeyToPlugin(
{ plugin: subPluginID },
{
sender: testAccount,
boxes,
sendParams: { fee: algokit.microAlgos(2_000) },
accounts: [aliceAbstractedAccount, joe],
}
)
// Step two: Call the plugin
.addTransaction({ transaction: makePaymentTxn, signer: testAccount })
// Step three: Call verify auth addr to rekey back to the abstracted account
.arc58VerifyAuthAddr({})
.execute();
// Verify the payment was made
const alicePostBalance = await algod.accountInformation(aliceAbstractedAccount).do();
const joePostBalance = await algod.accountInformation(joe).do();
expect(alicePostBalance.amount).toBe(alicePreBalance.amount - 100_000);
expect(joePostBalance.amount).toBe(joePreBalance.amount + 100_000);
});
});
describe('Named OptIn Plugin', () => {
let bob: algosdk.Account;
let asset: number;
const nameBox = new Uint8Array(Buffer.concat([Buffer.from('n'), Buffer.from('optIn')]));
let pluginBox: Uint8Array;
const boxes: Uint8Array[] = [nameBox];
beforeAll(async () => {
bob = fixture.context.testAccount;
const { algod } = fixture.context;
// Create an asset
const assetCreateTxn = algosdk.makeAssetCreateTxnWithSuggestedParamsFromObject({
from: bob.addr,
total: 1,
decimals: 0,
defaultFrozen: false,
suggestedParams,
});
const txn = await algokit.sendTransaction({ transaction: assetCreateTxn, from: bob }, algod);
asset = Number(txn.confirmation!.assetIndex!);
pluginBox = new Uint8Array(
Buffer.concat([
Buffer.from('p'),
Buffer.from(algosdk.encodeUint64(optInPluginID)),
algosdk.decodeAddress(ZERO_ADDRESS).publicKey,
])
);
boxes.push(pluginBox);
});
test('Alice adds the app to the abstracted account', async () => {
await abstractedAccountClient.appClient.fundAppAccount({ amount: algokit.microAlgos(43000) });
// Add opt-in plugin
await abstractedAccountClient.arc58AddNamedPlugin(
{ name: 'optIn', app: optInPluginID, allowedCaller: ZERO_ADDRESS, end: maxUint64 },
{ boxes }
);
});
test("Bob opts Alice's abstracted account into the asset", async () => {
// Form a payment from bob to alice's abstracted account to cover the MBR
const mbrPayment = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
from: bob.addr,
to: aliceAbstractedAccount,
amount: 200_000,
suggestedParams,
});
// Form the group txn needed to call the opt-in plugin
const optInGroup = (
await optInPluginClient
.compose()
.optInToAsset(
{ sender: aliceAbstractedAccount, asset, mbrPayment },
{ sender: bob, sendParams: { fee: algokit.microAlgos(2000) } }
)
.atc()
).buildGroup();
optInGroup.forEach((txn) => {
// eslint-disable-next-line no-param-reassign
txn.txn.group = undefined;
});
// Compose the group needed to actually use the plugin
await abstractedAccountClient
.compose()
// Rekey to the opt-in plugin
.arc58RekeyToNamedPlugin(
{ name: 'optIn' },
{
boxes,
sendParams: { fee: algokit.microAlgos(2000) },
assets: [asset],
}
)
// Add the mbr payment
.addTransaction({ transaction: optInGroup[0].txn, signer: bob }) // mbrPayment
// Add the opt-in plugin call
.addTransaction({ transaction: optInGroup[1].txn, signer: bob }) // optInToAsset
// Call verify auth addr to verify the abstracted account is rekeyed back to itself
.arc58VerifyAuthAddr({})
.execute();
});
});
});