forked from circle-Fi/circleFi-app
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchain.js
More file actions
204 lines (182 loc) · 7.93 KB
/
Copy pathchain.js
File metadata and controls
204 lines (182 loc) · 7.93 KB
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
/**
* Everything that talks to Stellar. Reads are simulated against a public RPC
* node, so they need no wallet, no account and no fee; writes are transactions
* the member's own wallet signs. There is no CircleFi server anywhere in here.
*/
import {
Account, Address, BASE_FEE, Contract, Networks, TransactionBuilder,
nativeToScVal, rpc, scValToNative,
} from '@stellar/stellar-sdk';
import {
getAddress, getNetwork, isConnected, requestAccess, signTransaction,
} from '@stellar/freighter-api';
export const RPC_URL = 'https://soroban-testnet.stellar.org';
export const PASSPHRASE = Networks.TESTNET;
const NULL_ACCOUNT = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';
/** Set at build time; falls back to the deployed testnet factory. */
export const FACTORY =
import.meta.env.VITE_FACTORY_ID || 'CCJRXTYIEFE6Z7DGTAKRGBLOGYBZNOONHI7FWZUDXEKZ7LGEGZNXKG3M';
export const server = new rpc.Server(RPC_URL);
/** Contract error codes, so a failure reaches a person as a sentence. */
const CIRCLE_ERRORS = {
1: 'Contribution or round length was zero',
2: 'Circle size must be between 3 and 24',
3: 'This circle has already filled and started',
4: 'This circle is not running',
5: 'This circle has not finished yet',
6: 'You are not a member of this circle',
7: 'You have already joined this circle',
8: 'You have already contributed to this round',
9: 'The round is still open and not everyone has paid',
10: 'Your deposit is already whole',
11: 'Nothing left to withdraw',
};
export class ChainError extends Error {
constructor(message, code) {
super(message);
this.name = 'ChainError';
this.code = code;
}
}
export function explain(e) {
if (e instanceof ChainError) return e.message;
const blob = `${e?.message || ''} ${(() => { try { return JSON.stringify(e); } catch { return ''; } })()}`;
const m = /Error\(Contract,\s*#(\d+)\)/.exec(blob);
if (m && CIRCLE_ERRORS[+m[1]]) return CIRCLE_ERRORS[+m[1]];
if (/insufficient balance|balance is not sufficient/i.test(blob))
return 'Not enough of that token in your wallet for this.';
if (/trustline|not authorized/i.test(blob))
return 'Your account cannot hold this token yet - add a trustline for it first.';
if (/account not found|NOT_FOUND/i.test(blob))
return 'That account does not exist on this network yet. Fund it with friendbot first.';
return e?.message || String(e);
}
// ---- argument helpers ----
export const addr = (a) => new Address(a).toScVal();
export const u32 = (n) => nativeToScVal(Number(n), { type: 'u32' });
export const u64 = (n) => nativeToScVal(BigInt(n), { type: 'u64' });
export const i128 = (n) => nativeToScVal(BigInt(n), { type: 'i128' });
// ---- reads ----
export async function read(contractId, method, args = []) {
const source = new Account(NULL_ACCOUNT, '0');
const tx = new TransactionBuilder(source, { fee: BASE_FEE, networkPassphrase: PASSPHRASE })
.addOperation(new Contract(contractId).call(method, ...args))
.setTimeout(30)
.build();
const sim = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(sim)) throw new ChainError(explain({ message: sim.error }));
const retval = sim.result?.retval;
return retval ? scValToNative(retval) : undefined;
}
// ---- writes ----
export async function invoke(contractId, method, args, wallet) {
if (!wallet) throw new ChainError('Connect a wallet first.');
const wrong = await wrongNetwork();
if (wrong) throw new ChainError(`Freighter is set to ${wrong}. Switch it to Test Net, then try again.`);
const account = await server.getAccount(wallet);
const built = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: PASSPHRASE })
.addOperation(new Contract(contractId).call(method, ...args))
.setTimeout(180)
.build();
// Simulating first means a call that would fail does so before the wallet
// prompt, rather than after the fee is spent.
const prepared = await server.prepareTransaction(built);
const res = await signTransaction(prepared.toXDR(), { networkPassphrase: PASSPHRASE, address: wallet });
if (res.error) throw new ChainError(String(res.error));
const sent = await server.sendTransaction(
TransactionBuilder.fromXDR(res.signedTxXdr, PASSPHRASE),
);
if (sent.status === 'ERROR') throw new ChainError(explain(sent.errorResult ?? sent));
const done = await server.pollTransaction(sent.hash, {
attempts: 30,
sleepStrategy: rpc.LinearSleepStrategy,
});
if (done.status !== 'SUCCESS') throw new ChainError(explain(done.resultXdr ?? done.status));
return {
hash: sent.hash,
value: done.returnValue ? scValToNative(done.returnValue) : undefined,
};
}
// ---- wallet ----
/**
* Freighter answers by posting to a content script; with no extension nothing
* ever answers, so bound the wait instead of letting the UI hang.
*/
export async function walletAvailable(timeoutMs = 1500) {
try {
return await Promise.race([
isConnected().then((r) => Boolean(r?.isConnected)),
new Promise((r) => setTimeout(() => r(false), timeoutMs)),
]);
} catch {
return false;
}
}
export async function connectWallet() {
if (!(await walletAvailable())) {
throw new ChainError('No Stellar wallet detected. Install Freighter, then reload this page.');
}
const res = await requestAccess();
if (res.error) throw new ChainError(String(res.error));
const wrong = await wrongNetwork();
if (wrong) throw new ChainError(`Freighter is set to ${wrong}. Switch it to Test Net, then connect again.`);
return res.address || (await getAddress()).address;
}
/**
* Freighter opens on the public network out of the box, and a signature made
* there is worthless here - the failure it produces names neither the cause nor
* the cure. So ask the extension which network it is on before anyone signs.
* Returns the offending network's name, or an empty string when all is well.
*/
export async function wrongNetwork() {
try {
// Same trap as isConnected: with the extension gone nothing ever answers.
const n = await Promise.race([
getNetwork(),
new Promise((r) => setTimeout(() => r(null), 2000)),
]);
if (!n || n.error || !n.networkPassphrase) return '';
return n.networkPassphrase === PASSPHRASE ? '' : (n.network || 'another network');
} catch {
return '';
}
}
// ---- domain calls ----
export const factory = {
count: () => read(FACTORY, 'count'),
list: (offset, limit) => read(FACTORY, 'list', [u32(offset), u32(limit)]),
create: (wallet, { token, contribution, roundSeconds, capacity }) =>
invoke(FACTORY, 'create',
[addr(wallet), addr(token), i128(contribution), u64(roundSeconds), u32(capacity)], wallet),
};
export const circle = {
config: (id) => read(id, 'get_config'),
state: (id) => read(id, 'get_state'),
member: (id, who) => read(id, 'get_member', [addr(who)]),
hasContributed: (id, round, who) => read(id, 'has_contributed', [u32(round), addr(who)]),
join: (id, wallet) => invoke(id, 'join', [addr(wallet)], wallet),
contribute: (id, wallet) => invoke(id, 'contribute', [addr(wallet)], wallet),
topUp: (id, wallet) => invoke(id, 'top_up', [addr(wallet)], wallet),
settle: (id, wallet) => invoke(id, 'settle', [], wallet),
withdraw: (id, wallet) => invoke(id, 'withdraw_deposit', [addr(wallet)], wallet),
};
const tokenMetaCache = new Map();
export async function tokenMeta(tokenId) {
if (tokenMetaCache.has(tokenId)) return tokenMetaCache.get(tokenId);
const [decimals, symbol] = await Promise.all([
read(tokenId, 'decimals').catch(() => 7),
read(tokenId, 'symbol').catch(() => 'token'),
]);
const meta = { decimals: Number(decimals), symbol: String(symbol) };
tokenMetaCache.set(tokenId, meta);
return meta;
}
export async function tokenBalance(tokenId, who) {
try {
return BigInt(await read(tokenId, 'balance', [addr(who)]));
} catch {
return null;
}
}
/** The native asset's own contract - the default a new circle is priced in. */
export const NATIVE_SAC = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC';