@layerswap/wallet-stellar 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/constants.js +8 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/service/StellarConnectionService.js +205 -0
- package/dist/esm/service/StellarWalletConnectModule.js +218 -0
- package/dist/esm/service/createStellarConnection.js +63 -0
- package/dist/esm/service/stellarConnector.js +23 -0
- package/dist/esm/service/stellarKitManager.js +272 -0
- package/dist/esm/service/stellarStore.js +14 -0
- package/dist/esm/stellarBalanceProvider.js +49 -0
- package/dist/esm/stellarBalances.js +40 -0
- package/dist/esm/stellarGasProvider.js +22 -0
- package/dist/esm/stellarNetwork.js +34 -0
- package/dist/esm/stellarServers.js +51 -0
- package/dist/esm/transferProvider/createStellarTransfer.js +122 -0
- package/dist/esm/transferProvider/validateStellarXdr.js +249 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/constants.d.ts +5 -0
- package/dist/types/constants.d.ts.map +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/service/StellarConnectionService.d.ts +44 -0
- package/dist/types/service/StellarConnectionService.d.ts.map +1 -0
- package/dist/types/service/StellarWalletConnectModule.d.ts +86 -0
- package/dist/types/service/StellarWalletConnectModule.d.ts.map +1 -0
- package/dist/types/service/createStellarConnection.d.ts +7 -0
- package/dist/types/service/createStellarConnection.d.ts.map +1 -0
- package/dist/types/service/stellarConnector.d.ts +4 -0
- package/dist/types/service/stellarConnector.d.ts.map +1 -0
- package/dist/types/service/stellarKitManager.d.ts +37 -0
- package/dist/types/service/stellarKitManager.d.ts.map +1 -0
- package/dist/types/service/stellarStore.d.ts +24 -0
- package/dist/types/service/stellarStore.d.ts.map +1 -0
- package/dist/types/stellarBalanceProvider.d.ts +6 -0
- package/dist/types/stellarBalanceProvider.d.ts.map +1 -0
- package/dist/types/stellarBalances.d.ts +22 -0
- package/dist/types/stellarBalances.d.ts.map +1 -0
- package/dist/types/stellarGasProvider.d.ts +11 -0
- package/dist/types/stellarGasProvider.d.ts.map +1 -0
- package/dist/types/stellarNetwork.d.ts +7 -0
- package/dist/types/stellarNetwork.d.ts.map +1 -0
- package/dist/types/stellarServers.d.ts +5 -0
- package/dist/types/stellarServers.d.ts.map +1 -0
- package/dist/types/transferProvider/createStellarTransfer.d.ts +3 -0
- package/dist/types/transferProvider/createStellarTransfer.d.ts.map +1 -0
- package/dist/types/transferProvider/validateStellarXdr.d.ts +22 -0
- package/dist/types/transferProvider/validateStellarXdr.d.ts.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { ActionMessageType } from '@layerswap/widget-types';
|
|
2
|
+
import { AppSettings, isMobile, isValidStellarAddress } from '@layerswap/utils';
|
|
3
|
+
import { STELLAR_SESSION_KEY } from '../constants';
|
|
4
|
+
import { STELLAR_APPKIT_WALLET_CONNECT_ID, STELLAR_WALLET_CONNECT_ID, StellarWalletConnectChain, StellarWalletConnectModule, } from './StellarWalletConnectModule';
|
|
5
|
+
import { stellarStore } from './stellarStore';
|
|
6
|
+
const STELLAR_APPKIT_WALLET_CONNECT_STORAGE_PREFIX = 'layerswapStellarAppKitWalletConnect';
|
|
7
|
+
function readPersistedSession() {
|
|
8
|
+
if (typeof window === 'undefined')
|
|
9
|
+
return undefined;
|
|
10
|
+
try {
|
|
11
|
+
const raw = window.localStorage.getItem(STELLAR_SESSION_KEY);
|
|
12
|
+
if (!raw)
|
|
13
|
+
return undefined;
|
|
14
|
+
const value = JSON.parse(raw);
|
|
15
|
+
if (typeof value.walletId === 'string'
|
|
16
|
+
&& typeof value.address === 'string'
|
|
17
|
+
&& value.address.startsWith('G')
|
|
18
|
+
&& isValidStellarAddress(value.address)) {
|
|
19
|
+
return { walletId: value.walletId, address: value.address };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Storage is optional; an unavailable/corrupt session is ignored.
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function persistSession(session) {
|
|
28
|
+
if (typeof window === 'undefined')
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
if (session) {
|
|
32
|
+
window.localStorage.setItem(STELLAR_SESSION_KEY, JSON.stringify(session));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
window.localStorage.removeItem(STELLAR_SESSION_KEY);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Wallet state still works when storage is unavailable.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function toSnapshot(wallet) {
|
|
43
|
+
return {
|
|
44
|
+
id: wallet.id,
|
|
45
|
+
name: wallet.name,
|
|
46
|
+
type: wallet.type,
|
|
47
|
+
isAvailable: wallet.isAvailable,
|
|
48
|
+
isPlatformWrapper: wallet.isPlatformWrapper,
|
|
49
|
+
icon: wallet.icon,
|
|
50
|
+
url: wallet.url,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function walletError(name, message) {
|
|
54
|
+
const error = new Error(message);
|
|
55
|
+
error.name = name;
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
58
|
+
class StellarKitManager {
|
|
59
|
+
constructor() {
|
|
60
|
+
this.detachers = [];
|
|
61
|
+
this.generation = 0;
|
|
62
|
+
}
|
|
63
|
+
init(walletConnect) {
|
|
64
|
+
if (typeof window === 'undefined')
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
if (this.initPromise)
|
|
67
|
+
return this.initPromise;
|
|
68
|
+
const generation = ++this.generation;
|
|
69
|
+
stellarStore.getState().setInitializing();
|
|
70
|
+
this.initPromise = this.initialize(walletConnect, generation).catch(error => {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
if (generation === this.generation)
|
|
73
|
+
stellarStore.getState().setReady(message);
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
return this.initPromise;
|
|
77
|
+
}
|
|
78
|
+
async initialize(walletConnect, generation) {
|
|
79
|
+
const kitPromise = import('@creit.tech/stellar-wallets-kit/sdk');
|
|
80
|
+
const kitTypesPromise = import('@creit.tech/stellar-wallets-kit/types');
|
|
81
|
+
const kitUtilsPromise = import('@creit.tech/stellar-wallets-kit/modules/utils');
|
|
82
|
+
const [kitModule, kitTypesModule, kitUtilsModule] = await Promise.all([
|
|
83
|
+
kitPromise,
|
|
84
|
+
kitTypesPromise,
|
|
85
|
+
kitUtilsPromise,
|
|
86
|
+
]);
|
|
87
|
+
if (generation !== this.generation)
|
|
88
|
+
return;
|
|
89
|
+
const isTestnet = AppSettings.ApiVersion === 'testnet' || AppSettings.ApiVersion === 'sandbox';
|
|
90
|
+
const network = isTestnet ? kitTypesModule.Networks.TESTNET : kitTypesModule.Networks.PUBLIC;
|
|
91
|
+
const modules = kitUtilsModule.defaultModules();
|
|
92
|
+
if (walletConnect?.projectId) {
|
|
93
|
+
this.walletConnectModule = new StellarWalletConnectModule(walletConnect, undefined, [isTestnet ? StellarWalletConnectChain.Testnet : StellarWalletConnectChain.Public,]);
|
|
94
|
+
modules.push(this.walletConnectModule);
|
|
95
|
+
this.detachers.push(this.walletConnectModule.onSessionDelete(() => this.clearSession()));
|
|
96
|
+
if (isMobile()) {
|
|
97
|
+
const { WalletConnectModule, WalletConnectTargetChain } = await import('@creit.tech/stellar-wallets-kit/modules/wallet-connect');
|
|
98
|
+
if (generation !== this.generation)
|
|
99
|
+
return;
|
|
100
|
+
const appKitModule = new WalletConnectModule({
|
|
101
|
+
projectId: walletConnect.projectId,
|
|
102
|
+
metadata: { name: walletConnect.name, description: walletConnect.description, url: walletConnect.url, icons: walletConnect.icons, },
|
|
103
|
+
signClientOptions: { customStoragePrefix: STELLAR_APPKIT_WALLET_CONNECT_STORAGE_PREFIX },
|
|
104
|
+
allowedChains: [isTestnet ? WalletConnectTargetChain.TESTNET : WalletConnectTargetChain.PUBLIC],
|
|
105
|
+
});
|
|
106
|
+
appKitModule.productId = STELLAR_APPKIT_WALLET_CONNECT_ID;
|
|
107
|
+
this.appKitWalletConnectModule = appKitModule;
|
|
108
|
+
modules.push(appKitModule);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const persisted = readPersistedSession();
|
|
112
|
+
kitModule.StellarWalletsKit.init({
|
|
113
|
+
modules,
|
|
114
|
+
selectedWalletId: persisted?.walletId,
|
|
115
|
+
network,
|
|
116
|
+
});
|
|
117
|
+
this.kit = kitModule.StellarWalletsKit;
|
|
118
|
+
this.networks = kitTypesModule.Networks;
|
|
119
|
+
this.network = network;
|
|
120
|
+
this.attachEvents(kitModule, kitTypesModule);
|
|
121
|
+
const wallets = await this.kit.refreshSupportedWallets();
|
|
122
|
+
if (generation !== this.generation)
|
|
123
|
+
return;
|
|
124
|
+
stellarStore.getState().setWallets(wallets.map(toSnapshot));
|
|
125
|
+
if (persisted && wallets.some(wallet => wallet.id === persisted.walletId)) {
|
|
126
|
+
this.selectedWalletId = persisted.walletId;
|
|
127
|
+
this.kit.setWallet(persisted.walletId);
|
|
128
|
+
stellarStore.getState().setActive(persisted.walletId, persisted.address);
|
|
129
|
+
}
|
|
130
|
+
else if (persisted) {
|
|
131
|
+
persistSession(undefined);
|
|
132
|
+
}
|
|
133
|
+
stellarStore.getState().setReady();
|
|
134
|
+
}
|
|
135
|
+
attachEvents(kitModule, kitTypesModule) {
|
|
136
|
+
const kit = kitModule.StellarWalletsKit;
|
|
137
|
+
this.detachers.push(kit.on(kitTypesModule.KitEventType.STATE_UPDATED, event => {
|
|
138
|
+
stellarStore.getState().setNetworkPassphrase(event.payload.networkPassphrase || undefined);
|
|
139
|
+
const address = event.payload.address;
|
|
140
|
+
const walletId = this.selectedWalletId ?? stellarStore.getState().activeWalletId;
|
|
141
|
+
if (walletId && address?.startsWith('G') && isValidStellarAddress(address)) {
|
|
142
|
+
stellarStore.getState().setActive(walletId, address);
|
|
143
|
+
persistSession({ walletId, address });
|
|
144
|
+
}
|
|
145
|
+
}), kit.on(kitTypesModule.KitEventType.WALLET_SELECTED, event => {
|
|
146
|
+
this.selectedWalletId = event.payload.id;
|
|
147
|
+
}), kit.on(kitTypesModule.KitEventType.DISCONNECT, () => this.clearSession()));
|
|
148
|
+
}
|
|
149
|
+
async connect(walletId) {
|
|
150
|
+
await this.requireReady();
|
|
151
|
+
const kit = this.requireKit();
|
|
152
|
+
const previousWalletId = stellarStore.getState().activeWalletId;
|
|
153
|
+
try {
|
|
154
|
+
this.selectedWalletId = walletId;
|
|
155
|
+
kit.setWallet(walletId);
|
|
156
|
+
const { address } = await kit.fetchAddress();
|
|
157
|
+
this.assertSourceAddress(address);
|
|
158
|
+
if (this.network)
|
|
159
|
+
await this.assertWalletNetwork(this.network);
|
|
160
|
+
stellarStore.getState().setActive(walletId, address);
|
|
161
|
+
persistSession({ walletId, address });
|
|
162
|
+
return { address };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
this.selectedWalletId = previousWalletId;
|
|
166
|
+
if (previousWalletId)
|
|
167
|
+
kit.setWallet(previousWalletId);
|
|
168
|
+
throw error instanceof Error ? error : new Error(error.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
onDisplayUri(listener) {
|
|
172
|
+
return this.walletConnectModule?.onDisplayUri(listener) ?? (() => { });
|
|
173
|
+
}
|
|
174
|
+
warmUpWalletConnect() {
|
|
175
|
+
this.walletConnectModule?.warmup();
|
|
176
|
+
}
|
|
177
|
+
async revalidate(expectedAddress, networkPassphrase) {
|
|
178
|
+
await this.requireReady();
|
|
179
|
+
const kit = this.requireKit();
|
|
180
|
+
const { activeWalletId } = stellarStore.getState();
|
|
181
|
+
if (!activeWalletId) {
|
|
182
|
+
throw walletError(ActionMessageType.WaletMismatch, 'Stellar wallet is not connected');
|
|
183
|
+
}
|
|
184
|
+
this.selectedWalletId = activeWalletId;
|
|
185
|
+
kit.setWallet(activeWalletId);
|
|
186
|
+
await this.assertWalletNetwork(networkPassphrase);
|
|
187
|
+
const { address } = activeWalletId === STELLAR_WALLET_CONNECT_ID && this.walletConnectModule
|
|
188
|
+
? await this.walletConnectModule.getConnectedAddress(expectedAddress)
|
|
189
|
+
: activeWalletId === STELLAR_APPKIT_WALLET_CONNECT_ID && this.appKitWalletConnectModule
|
|
190
|
+
? await this.getAppKitConnectedAddress(this.appKitWalletConnectModule, expectedAddress)
|
|
191
|
+
: await kit.fetchAddress().catch((error) => { throw new Error(error.message); });
|
|
192
|
+
this.assertSourceAddress(address);
|
|
193
|
+
if (address !== expectedAddress) {
|
|
194
|
+
throw walletError(ActionMessageType.WaletMismatch, 'The connected Stellar account changed');
|
|
195
|
+
}
|
|
196
|
+
stellarStore.getState().setActive(activeWalletId, address);
|
|
197
|
+
persistSession({ walletId: activeWalletId, address });
|
|
198
|
+
}
|
|
199
|
+
async signTransaction(xdr, networkPassphrase, address) {
|
|
200
|
+
const kit = this.requireKit();
|
|
201
|
+
const networks = this.networks;
|
|
202
|
+
if (networks) {
|
|
203
|
+
const knownNetwork = Object.values(networks).find(value => value === networkPassphrase);
|
|
204
|
+
if (knownNetwork)
|
|
205
|
+
kit.setNetwork(knownNetwork);
|
|
206
|
+
}
|
|
207
|
+
return kit.signTransaction(xdr, { networkPassphrase, address })
|
|
208
|
+
.catch((error) => { throw new Error(error.message); });
|
|
209
|
+
}
|
|
210
|
+
async refreshWallets() {
|
|
211
|
+
await this.requireReady();
|
|
212
|
+
const wallets = await this.requireKit().refreshSupportedWallets();
|
|
213
|
+
stellarStore.getState().setWallets(wallets.map(toSnapshot));
|
|
214
|
+
}
|
|
215
|
+
async disconnect() {
|
|
216
|
+
try {
|
|
217
|
+
await this.kit?.disconnect();
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
this.clearSession();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
dispose() {
|
|
224
|
+
this.generation += 1;
|
|
225
|
+
for (const detach of this.detachers.splice(0))
|
|
226
|
+
detach();
|
|
227
|
+
this.walletConnectModule?.dispose();
|
|
228
|
+
this.walletConnectModule = undefined;
|
|
229
|
+
this.appKitWalletConnectModule = undefined;
|
|
230
|
+
this.kit = undefined;
|
|
231
|
+
this.networks = undefined;
|
|
232
|
+
this.initPromise = undefined;
|
|
233
|
+
this.selectedWalletId = undefined;
|
|
234
|
+
}
|
|
235
|
+
async requireReady() {
|
|
236
|
+
if (!this.initPromise)
|
|
237
|
+
throw new Error('Stellar Wallets Kit is not initialized');
|
|
238
|
+
await this.initPromise;
|
|
239
|
+
}
|
|
240
|
+
requireKit() {
|
|
241
|
+
if (!this.kit)
|
|
242
|
+
throw new Error('Stellar Wallets Kit is not initialized');
|
|
243
|
+
return this.kit;
|
|
244
|
+
}
|
|
245
|
+
async assertWalletNetwork(expected) {
|
|
246
|
+
const walletNetwork = await this.requireKit().getNetwork().catch(() => undefined);
|
|
247
|
+
if (walletNetwork?.networkPassphrase && walletNetwork.networkPassphrase !== expected) {
|
|
248
|
+
const target = expected === this.networks?.TESTNET ? 'Testnet' : 'Mainnet';
|
|
249
|
+
throw walletError(ActionMessageType.WaletMismatch, `The wallet is on the wrong network. Switch it to Stellar ${target}, then try again`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async getAppKitConnectedAddress(module, expectedAddress) {
|
|
253
|
+
const sessions = await module.getSessions();
|
|
254
|
+
const connected = sessions.some(session => (session.namespaces['stellar']?.accounts ?? []).some(account => account.split(':')[2] === expectedAddress));
|
|
255
|
+
if (!connected) {
|
|
256
|
+
throw new Error('The Stellar WalletConnect session expired; reconnect the wallet');
|
|
257
|
+
}
|
|
258
|
+
return { address: expectedAddress };
|
|
259
|
+
}
|
|
260
|
+
assertSourceAddress(address) {
|
|
261
|
+
if (!address.startsWith('G') || !isValidStellarAddress(address)) {
|
|
262
|
+
throw new Error('The selected wallet did not return a valid Stellar G-address');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
clearSession() {
|
|
266
|
+
persistSession(undefined);
|
|
267
|
+
this.selectedWalletId = undefined;
|
|
268
|
+
stellarStore.getState().setActive(undefined, undefined);
|
|
269
|
+
stellarStore.getState().setNetworkPassphrase(undefined);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export const stellarKitManager = new StellarKitManager();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createStore } from 'zustand/vanilla';
|
|
2
|
+
export const stellarStore = createStore()((set) => ({
|
|
3
|
+
wallets: [],
|
|
4
|
+
activeWalletId: undefined,
|
|
5
|
+
activeAddress: undefined,
|
|
6
|
+
networkPassphrase: undefined,
|
|
7
|
+
ready: false,
|
|
8
|
+
error: undefined,
|
|
9
|
+
setWallets: wallets => set({ wallets }),
|
|
10
|
+
setActive: (activeWalletId, activeAddress) => set({ activeWalletId, activeAddress }),
|
|
11
|
+
setNetworkPassphrase: networkPassphrase => set({ networkPassphrase }),
|
|
12
|
+
setInitializing: () => set({ ready: false, error: undefined }),
|
|
13
|
+
setReady: error => set({ ready: true, error }),
|
|
14
|
+
}));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BalanceProvider, insertIfNotExists } from '@layerswap/widget-types';
|
|
2
|
+
import { NetworkType } from '@layerswap/widget-types';
|
|
3
|
+
import { createStellarZeroBalance, createUnfundedStellarBalances, resolveStellarBalanceAmount, } from './stellarBalances';
|
|
4
|
+
import { resolveStellarAsset, resolveStellarNetworkPassphrase } from './stellarNetwork';
|
|
5
|
+
import { getStellarHorizonServer } from './stellarServers';
|
|
6
|
+
function statusOf(error) {
|
|
7
|
+
const candidate = error;
|
|
8
|
+
return candidate?.status ?? candidate?.response?.status ?? candidate?.cause?.status ?? candidate?.cause?.response?.status;
|
|
9
|
+
}
|
|
10
|
+
export class StellarBalanceProvider extends BalanceProvider {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.supportsNetwork = network => network.type === NetworkType.Stellar;
|
|
14
|
+
this.fetchBalance = async (address, network) => {
|
|
15
|
+
const tokens = insertIfNotExists(network.tokens, network.token);
|
|
16
|
+
try {
|
|
17
|
+
const networkPassphrase = resolveStellarNetworkPassphrase(network);
|
|
18
|
+
const server = await getStellarHorizonServer(network, networkPassphrase);
|
|
19
|
+
const needsReserve = tokens.some(token => resolveStellarAsset(token).isNative());
|
|
20
|
+
const [account, ledgerPage] = await Promise.all([
|
|
21
|
+
server.loadAccount(address),
|
|
22
|
+
needsReserve ? server.ledgers().order('desc').limit(1).call() : Promise.resolve(undefined),
|
|
23
|
+
]);
|
|
24
|
+
const baseReserveInStroops = ledgerPage?.records[0]?.base_reserve_in_stroops;
|
|
25
|
+
if (needsReserve && !Number.isSafeInteger(baseReserveInStroops)) {
|
|
26
|
+
throw new Error('Horizon returned an invalid Stellar base reserve');
|
|
27
|
+
}
|
|
28
|
+
return tokens.map(token => {
|
|
29
|
+
try {
|
|
30
|
+
return {
|
|
31
|
+
...createStellarZeroBalance(token, network),
|
|
32
|
+
amount: resolveStellarBalanceAmount(token, account.balances, resolveStellarAsset(token).isNative()
|
|
33
|
+
? { account, baseReserveInStroops: baseReserveInStroops }
|
|
34
|
+
: undefined),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return this.resolveTokenBalanceFetchError(error, token, network);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (statusOf(error) === 404)
|
|
44
|
+
return createUnfundedStellarBalances(tokens, network);
|
|
45
|
+
return tokens.map(token => this.resolveTokenBalanceFetchError(error, token, network));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { baseUnitsToNumber } from '@layerswap/utils/common';
|
|
2
|
+
import { resolveStellarAsset } from './stellarNetwork';
|
|
3
|
+
export function createStellarZeroBalance(token, network) {
|
|
4
|
+
const asset = resolveStellarAsset(token);
|
|
5
|
+
return {
|
|
6
|
+
network: network.name,
|
|
7
|
+
token: token.symbol,
|
|
8
|
+
amount: 0,
|
|
9
|
+
request_time: new Date().toJSON(),
|
|
10
|
+
decimals: token.decimals,
|
|
11
|
+
isNativeCurrency: asset.isNative(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function resolveStellarBalanceAmount(token, balances, reserve) {
|
|
15
|
+
const asset = resolveStellarAsset(token);
|
|
16
|
+
const line = asset.isNative()
|
|
17
|
+
? balances.find(balance => balance.asset_type === 'native')
|
|
18
|
+
: balances.find(balance => (balance.asset_type === asset.getAssetType()
|
|
19
|
+
&& balance.asset_code === asset.getCode()
|
|
20
|
+
&& balance.asset_issuer === asset.getIssuer()));
|
|
21
|
+
if (!line)
|
|
22
|
+
return 0;
|
|
23
|
+
if (!asset.isNative() && line.is_authorized !== true) {
|
|
24
|
+
throw new Error(`The ${token.symbol} trustline is not authorized`);
|
|
25
|
+
}
|
|
26
|
+
let spendable = Number(line.balance) - Number(line.selling_liabilities ?? 0);
|
|
27
|
+
if (asset.isNative()) {
|
|
28
|
+
if (!reserve)
|
|
29
|
+
throw new Error('Stellar base reserve is unavailable');
|
|
30
|
+
const { account, baseReserveInStroops } = reserve;
|
|
31
|
+
const reserveUnits = 2 + account.subentry_count + account.num_sponsoring - account.num_sponsored;
|
|
32
|
+
spendable -= reserveUnits * baseUnitsToNumber(BigInt(baseReserveInStroops), token.decimals);
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isFinite(spendable))
|
|
35
|
+
throw new Error(`Horizon returned an invalid ${token.symbol} balance`);
|
|
36
|
+
return Math.max(0, spendable);
|
|
37
|
+
}
|
|
38
|
+
export function createUnfundedStellarBalances(tokens, network) {
|
|
39
|
+
return tokens.map(token => createStellarZeroBalance(token, network));
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { baseUnitsToNumber } from '@layerswap/utils/common';
|
|
2
|
+
import { NetworkType } from '@layerswap/widget-types';
|
|
3
|
+
import { resolveStellarNetworkPassphrase } from './stellarNetwork';
|
|
4
|
+
import { getStellarHorizonServer } from './stellarServers';
|
|
5
|
+
export class StellarGasProvider {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.supportsNetwork = network => network.type === NetworkType.Stellar;
|
|
8
|
+
}
|
|
9
|
+
async getGas({ network }) {
|
|
10
|
+
if (!network.token)
|
|
11
|
+
throw new Error('Stellar network token is missing');
|
|
12
|
+
const networkPassphrase = resolveStellarNetworkPassphrase(network);
|
|
13
|
+
const server = await getStellarHorizonServer(network, networkPassphrase);
|
|
14
|
+
const feeInStroops = await server.fetchBaseFee();
|
|
15
|
+
if (!Number.isSafeInteger(feeInStroops) || feeInStroops < 0)
|
|
16
|
+
throw new Error('Invalid Stellar fee');
|
|
17
|
+
return {
|
|
18
|
+
gas: baseUnitsToNumber(BigInt(feeInStroops), network.token.decimals),
|
|
19
|
+
token: network.token,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Asset, StrKey } from '@stellar/stellar-sdk';
|
|
2
|
+
export const STELLAR_PUBLIC_PASSPHRASE = 'Public Global Stellar Network ; September 2015';
|
|
3
|
+
export const STELLAR_TESTNET_PASSPHRASE = 'Test SDF Network ; September 2015';
|
|
4
|
+
export function resolveStellarNetworkPassphrase(network) {
|
|
5
|
+
const expected = network.name === 'STELLAR_MAINNET'
|
|
6
|
+
? STELLAR_PUBLIC_PASSPHRASE
|
|
7
|
+
: network.name === 'STELLAR_TESTNET'
|
|
8
|
+
? STELLAR_TESTNET_PASSPHRASE
|
|
9
|
+
: undefined;
|
|
10
|
+
const configured = network.chain_id;
|
|
11
|
+
if (!expected)
|
|
12
|
+
throw new Error(`Unsupported Stellar network: ${network.name}`);
|
|
13
|
+
if (!configured)
|
|
14
|
+
throw new Error('Stellar network passphrase is missing from chain_id');
|
|
15
|
+
if (configured !== expected)
|
|
16
|
+
throw new Error('Stellar network passphrase does not match the network ID');
|
|
17
|
+
return configured;
|
|
18
|
+
}
|
|
19
|
+
export function resolveStellarAsset(token) {
|
|
20
|
+
if (token.decimals !== 7)
|
|
21
|
+
throw new Error(`Stellar assets must use 7 decimals: ${token.symbol}`);
|
|
22
|
+
if (!token.contract) {
|
|
23
|
+
if (token.symbol !== 'XLM')
|
|
24
|
+
throw new Error(`Unsupported native Stellar asset: ${token.symbol}`);
|
|
25
|
+
return Asset.native();
|
|
26
|
+
}
|
|
27
|
+
if (token.symbol.length < 1 || token.symbol.length > 12 || !/^[a-zA-Z0-9]+$/.test(token.symbol)) {
|
|
28
|
+
throw new Error(`Stellar issued-asset code is invalid: ${token.symbol}`);
|
|
29
|
+
}
|
|
30
|
+
if (!StrKey.isValidEd25519PublicKey(token.contract)) {
|
|
31
|
+
throw new Error(`Stellar issued-asset issuer is invalid: ${token.symbol}`);
|
|
32
|
+
}
|
|
33
|
+
return new Asset(token.symbol, token.contract);
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Horizon, rpc } from '@stellar/stellar-sdk';
|
|
2
|
+
const horizonServers = new Map();
|
|
3
|
+
const rpcServers = new Map();
|
|
4
|
+
function getCandidateUrls(network) {
|
|
5
|
+
return [...new Set([network.node_url, ...(network.nodes ?? [])].filter(Boolean))];
|
|
6
|
+
}
|
|
7
|
+
async function findServer(network, networkPassphrase, kind, createAndVerify) {
|
|
8
|
+
const candidates = getCandidateUrls(network);
|
|
9
|
+
if (candidates.length === 0) {
|
|
10
|
+
throw new Error(`No Stellar ${kind} endpoints are configured for ${network.name}`);
|
|
11
|
+
}
|
|
12
|
+
const results = await Promise.allSettled(candidates.map(createAndVerify));
|
|
13
|
+
const match = results.find(result => result.status === 'fulfilled');
|
|
14
|
+
if (match?.status === 'fulfilled')
|
|
15
|
+
return match.value;
|
|
16
|
+
const cause = results.find((result) => result.status === 'rejected')?.reason;
|
|
17
|
+
throw new Error(`No Stellar ${kind} endpoint for ${network.name} matches ${networkPassphrase}`, { cause });
|
|
18
|
+
}
|
|
19
|
+
function cachedServer(cache, key, load) {
|
|
20
|
+
const existing = cache.get(key);
|
|
21
|
+
if (existing)
|
|
22
|
+
return existing;
|
|
23
|
+
const pending = load().catch(error => {
|
|
24
|
+
cache.delete(key);
|
|
25
|
+
throw error;
|
|
26
|
+
});
|
|
27
|
+
cache.set(key, pending);
|
|
28
|
+
return pending;
|
|
29
|
+
}
|
|
30
|
+
export function getStellarHorizonServer(network, networkPassphrase) {
|
|
31
|
+
const key = `${network.name}:${networkPassphrase}:${getCandidateUrls(network).join('|')}`;
|
|
32
|
+
return cachedServer(horizonServers, key, () => findServer(network, networkPassphrase, 'Horizon', async (url) => {
|
|
33
|
+
const server = new Horizon.Server(url);
|
|
34
|
+
const root = await server.root();
|
|
35
|
+
if (root.network_passphrase !== networkPassphrase) {
|
|
36
|
+
throw new Error(`Horizon endpoint ${url} is connected to a different Stellar network`);
|
|
37
|
+
}
|
|
38
|
+
return server;
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
export function getStellarRpcServer(network, networkPassphrase) {
|
|
42
|
+
const key = `${network.name}:${networkPassphrase}:${getCandidateUrls(network).join('|')}`;
|
|
43
|
+
return cachedServer(rpcServers, key, () => findServer(network, networkPassphrase, 'RPC', async (url) => {
|
|
44
|
+
const server = new rpc.Server(url);
|
|
45
|
+
const rpcNetwork = await server.getNetwork();
|
|
46
|
+
if (rpcNetwork.passphrase !== networkPassphrase) {
|
|
47
|
+
throw new Error(`RPC endpoint ${url} is connected to a different Stellar network`);
|
|
48
|
+
}
|
|
49
|
+
return server;
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Transaction, TransactionBuilder, TransactionFailedError } from '@stellar/stellar-sdk';
|
|
2
|
+
import { bytesToHex } from '@layerswap/utils/common';
|
|
3
|
+
import { foregroundWalletApp } from '@layerswap/wallet-core';
|
|
4
|
+
import { ActionMessageType, NetworkType } from '@layerswap/widget-types';
|
|
5
|
+
import { resolveStellarNetworkPassphrase } from '../stellarNetwork';
|
|
6
|
+
import { getStellarHorizonServer, getStellarRpcServer } from '../stellarServers';
|
|
7
|
+
import { stellarKitManager } from '../service/stellarKitManager';
|
|
8
|
+
import { buildStellarDepositOperation, validateStellarXdr } from './validateStellarXdr';
|
|
9
|
+
const TRANSACTION_TIMEOUT_SECONDS = 5 * 60;
|
|
10
|
+
function mappedError(name, message, cause) {
|
|
11
|
+
const error = new Error(message, cause === undefined ? undefined : { cause });
|
|
12
|
+
error.name = name;
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
function isWalletRejection(error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return /reject|declin|cancel|denied|closed/i.test(message);
|
|
18
|
+
}
|
|
19
|
+
function classifyTransferError(error) {
|
|
20
|
+
if (error instanceof Error && Object.values(ActionMessageType).includes(error.name))
|
|
21
|
+
return error;
|
|
22
|
+
if (isWalletRejection(error)) {
|
|
23
|
+
return mappedError(ActionMessageType.TransactionRejected, 'The Stellar transaction was rejected', error);
|
|
24
|
+
}
|
|
25
|
+
if (error instanceof TransactionFailedError) {
|
|
26
|
+
const resultCodes = error.getResultCodes();
|
|
27
|
+
if (resultCodes.transaction === 'tx_insufficient_balance'
|
|
28
|
+
|| resultCodes.operations.some(code => code === 'op_underfunded' || code === 'op_low_reserve')) {
|
|
29
|
+
return mappedError(ActionMessageType.InsufficientFunds, 'Insufficient Stellar balance or reserve', error);
|
|
30
|
+
}
|
|
31
|
+
if (resultCodes.transaction === 'tx_bad_seq' || resultCodes.transaction === 'tx_too_late') {
|
|
32
|
+
// This failure happened after the user signed. Never classify it as
|
|
33
|
+
// a preflight expiry: the widget retries only unsigned stale actions.
|
|
34
|
+
return mappedError(ActionMessageType.TransactionFailed, 'The signed Stellar transaction became stale', error);
|
|
35
|
+
}
|
|
36
|
+
return mappedError(ActionMessageType.TransactionFailed, 'Horizon rejected the Stellar transaction', error);
|
|
37
|
+
}
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return mappedError(ActionMessageType.UnexpectedErrorMessage, message || 'Stellar transaction failed', error);
|
|
40
|
+
}
|
|
41
|
+
export function createStellarTransfer() {
|
|
42
|
+
return {
|
|
43
|
+
supportsNetwork: network => network.type === NetworkType.Stellar,
|
|
44
|
+
async executeTransfer(params) {
|
|
45
|
+
const { selectedWallet, depositAddress, network, token, amountInBaseUnits, encodedArgs, sequenceNumber, } = params;
|
|
46
|
+
if (!selectedWallet?.address)
|
|
47
|
+
throw new Error('Stellar wallet address not found');
|
|
48
|
+
if (!depositAddress)
|
|
49
|
+
throw new Error('Stellar depository contract not found');
|
|
50
|
+
if (!amountInBaseUnits)
|
|
51
|
+
throw new Error('Stellar deposit amount is missing');
|
|
52
|
+
if (!encodedArgs)
|
|
53
|
+
throw new Error('Stellar deposit encoded_args are missing');
|
|
54
|
+
if (sequenceNumber === undefined)
|
|
55
|
+
throw new Error('Stellar swap sequence number is missing');
|
|
56
|
+
try {
|
|
57
|
+
const networkPassphrase = resolveStellarNetworkPassphrase(network);
|
|
58
|
+
const operation = buildStellarDepositOperation({
|
|
59
|
+
networkPassphrase,
|
|
60
|
+
selectedAddress: selectedWallet.address,
|
|
61
|
+
depositoryContract: depositAddress,
|
|
62
|
+
token,
|
|
63
|
+
amountInBaseUnits,
|
|
64
|
+
encodedArgs,
|
|
65
|
+
swapSequenceNumber: sequenceNumber,
|
|
66
|
+
});
|
|
67
|
+
const [horizonServer, rpcServer] = await Promise.all([
|
|
68
|
+
getStellarHorizonServer(network, networkPassphrase),
|
|
69
|
+
getStellarRpcServer(network, networkPassphrase),
|
|
70
|
+
]);
|
|
71
|
+
const [account, baseFee] = await Promise.all([
|
|
72
|
+
horizonServer.loadAccount(selectedWallet.address),
|
|
73
|
+
horizonServer.fetchBaseFee(),
|
|
74
|
+
]);
|
|
75
|
+
if (!Number.isSafeInteger(baseFee) || baseFee <= 0) {
|
|
76
|
+
throw new Error('Horizon returned an invalid Stellar base fee');
|
|
77
|
+
}
|
|
78
|
+
const currentAccountSequence = account.sequence;
|
|
79
|
+
const preparedTransaction = await rpcServer.prepareTransaction(new TransactionBuilder(account, {
|
|
80
|
+
fee: baseFee.toString(),
|
|
81
|
+
networkPassphrase,
|
|
82
|
+
})
|
|
83
|
+
.addOperation(operation)
|
|
84
|
+
.setTimeout(TRANSACTION_TIMEOUT_SECONDS)
|
|
85
|
+
.build());
|
|
86
|
+
const preparedXdr = preparedTransaction.toXdr();
|
|
87
|
+
const unsignedTransaction = validateStellarXdr({
|
|
88
|
+
envelopeXdr: preparedXdr,
|
|
89
|
+
networkPassphrase,
|
|
90
|
+
selectedAddress: selectedWallet.address,
|
|
91
|
+
depositoryContract: depositAddress,
|
|
92
|
+
token,
|
|
93
|
+
amountInBaseUnits,
|
|
94
|
+
encodedArgs,
|
|
95
|
+
swapSequenceNumber: sequenceNumber,
|
|
96
|
+
currentAccountSequence,
|
|
97
|
+
});
|
|
98
|
+
await stellarKitManager.revalidate(selectedWallet.address, networkPassphrase);
|
|
99
|
+
await foregroundWalletApp(selectedWallet.metadata?.deepLink);
|
|
100
|
+
const signed = await stellarKitManager.signTransaction(preparedXdr, networkPassphrase, selectedWallet.address);
|
|
101
|
+
if (signed.signerAddress && signed.signerAddress !== selectedWallet.address) {
|
|
102
|
+
throw mappedError(ActionMessageType.WaletMismatch, 'The Stellar wallet signed with a different account');
|
|
103
|
+
}
|
|
104
|
+
const signedTransaction = TransactionBuilder.fromXdr(signed.signedTxXdr, networkPassphrase);
|
|
105
|
+
if (!(signedTransaction instanceof Transaction))
|
|
106
|
+
throw new Error('Wallet returned an unsupported fee-bump transaction');
|
|
107
|
+
if (signedTransaction.signatures.length === 0)
|
|
108
|
+
throw new Error('Wallet returned a Stellar transaction without a signature');
|
|
109
|
+
if (bytesToHex(signedTransaction.hash()) !== bytesToHex(unsignedTransaction.hash())) {
|
|
110
|
+
throw new Error('Wallet changed the Stellar transaction while signing');
|
|
111
|
+
}
|
|
112
|
+
const result = await horizonServer.submitTransaction(signedTransaction);
|
|
113
|
+
if (!result.successful || !result.hash)
|
|
114
|
+
throw new Error('Horizon did not accept the Stellar transaction');
|
|
115
|
+
return result.hash;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw classifyTransferError(error);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|