Skip to content

Commit b4cd83c

Browse files
authored
Merge pull request #106 from Ummi-001/fix/call-ephemeral-initialize
fix: call EphemeralAccount.initialize() after account creation (#76)
2 parents 66380fb + 54bc93f commit b4cd83c

File tree

3 files changed

+129
-18
lines changed

3 files changed

+129
-18
lines changed

docs/fix-76-ephemeral-account-initialize.md

Whitespace-only changes.

src/modules/accounts/accounts.service.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,14 @@ export class AccountsService {
106106

107107
// Create account on Stellar
108108
const txHash = await this.stellarService.createEphemeralAccount({
109-
publicKey: ephemeralKeypair.publicKey(),
110-
amount: createAccountDto.amount,
111-
asset: createAccountDto.asset,
112-
expiresAt,
113-
});
109+
publicKey: ephemeralKeypair.publicKey(),
110+
secretKey: ephemeralKeypair.secret(),
111+
amount: createAccountDto.amount,
112+
asset: createAccountDto.asset,
113+
expiresAt,
114+
expiresIn: createAccountDto.expiresIn,
115+
fundingSource: createAccountDto.fundingSource,
116+
});
114117

115118
// Generate claim token
116119
const claimToken = this.generateClaimToken(ephemeralKeypair.publicKey());

src/modules/stellar/stellar.service.ts

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,52 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { ConfigService } from '@nestjs/config';
33
import * as StellarSdk from '@stellar/stellar-sdk';
4-
4+
import {
5+
Contract,
6+
rpc,
7+
TransactionBuilder,
8+
BASE_FEE,
9+
Address,
10+
nativeToScVal,
11+
} from '@stellar/stellar-sdk';
512
@Injectable()
613
export class StellarService {
714
private readonly logger = new Logger(StellarService.name);
815
private server: StellarSdk.Horizon.Server;
16+
private sorobanServer: rpc.Server;
917
private network: string;
18+
private readonly contractId: string;
1019

1120
constructor(private configService: ConfigService) {
12-
const horizonUrl =
13-
this.configService.getOrThrow<string>('stellar.horizonUrl');
14-
this.network = this.configService.getOrThrow<string>('stellar.network');
15-
this.server = new StellarSdk.Horizon.Server(horizonUrl);
21+
const horizonUrl =
22+
this.configService.getOrThrow<string>('stellar.horizonUrl');
23+
this.network = this.configService.getOrThrow<string>('stellar.network');
24+
this.server = new StellarSdk.Horizon.Server(horizonUrl);
1625

17-
this.logger.log(`Initialized Stellar service for ${this.network}`);
18-
}
26+
const sorobanRpcUrl = this.configService.getOrThrow<string>(
27+
'stellar.sorobanRpcUrl',
28+
);
29+
this.sorobanServer = new rpc.Server(sorobanRpcUrl);
30+
this.contractId = this.configService.getOrThrow<string>(
31+
'stellar.contracts.ephemeralAccount',
32+
);
33+
34+
this.logger.log(`Initialized Stellar service for ${this.network}`);
35+
}
1936

2037
generateKeypair(): StellarSdk.Keypair {
2138
return StellarSdk.Keypair.random();
2239
}
2340

2441
async createEphemeralAccount(params: {
25-
publicKey: string;
26-
amount: string;
27-
asset: string;
28-
expiresAt: Date;
29-
}): Promise<string> {
42+
publicKey: string;
43+
secretKey: string;
44+
amount: string;
45+
asset: string;
46+
expiresAt: Date;
47+
expiresIn: number;
48+
fundingSource: string;
49+
}): Promise<string> : Promise<string> {
3050
this.logger.log(`Creating ephemeral account: ${params.publicKey}`);
3151

3252
const fundingSecret = this.configService.getOrThrow<string>(
@@ -57,9 +77,97 @@ export class StellarService {
5777
const result = await this.server.submitTransaction(transaction);
5878

5979
this.logger.log(`Account created: ${result.hash}`);
60-
return result.hash;
80+
81+
// Call initialize() on the Soroban contract immediately after account creation
82+
await this.initializeEphemeralAccount({
83+
ephemeralPublicKey: params.publicKey,
84+
ephemeralSecretKey: params.secretKey,
85+
expiresIn: params.expiresIn,
86+
fundingSource: params.fundingSource,
87+
});
88+
89+
return result.hash;
6190
}
91+
private async getCurrentLedger(): Promise<number> {
92+
const latestLedger = await this.sorobanServer.getLatestLedger();
93+
return latestLedger.sequence;
94+
}
95+
96+
private async initializeEphemeralAccount(params: {
97+
ephemeralPublicKey: string;
98+
ephemeralSecretKey: string;
99+
expiresIn: number;
100+
fundingSource: string;
101+
}): Promise<void> {
102+
this.logger.log(
103+
`Initializing contract for account: ${params.ephemeralPublicKey}`,
104+
);
105+
106+
// Get current ledger number from the blockchain
107+
const currentLedger = await this.getCurrentLedger();
108+
109+
// Stellar produces ~1 ledger every 5 seconds
110+
// Convert expiresIn (seconds) to ledger count
111+
const LEDGER_CLOSE_TIME_SECONDS = 5;
112+
const expiryLedger =
113+
currentLedger +
114+
Math.ceil(params.expiresIn / LEDGER_CLOSE_TIME_SECONDS);
115+
116+
// Build the keypair from the secret so we can sign the transaction
117+
const ephemeralKeypair = StellarSdk.Keypair.fromSecret(
118+
params.ephemeralSecretKey,
119+
);
120+
121+
// Load the ephemeral account from Soroban RPC
122+
const ephemeralAccount = await this.sorobanServer.getAccount(
123+
params.ephemeralPublicKey,
124+
);
62125

126+
// Create contract instance
127+
const contract = new Contract(this.contractId);
128+
129+
// Build the initialize() transaction
130+
const transaction = new TransactionBuilder(ephemeralAccount, {
131+
fee: BASE_FEE,
132+
networkPassphrase: this.getNetworkPassphrase(),
133+
})
134+
.addOperation(
135+
contract.call(
136+
'initialize',
137+
Address.fromString(params.ephemeralPublicKey).toScVal(), // creator
138+
nativeToScVal(expiryLedger, { type: 'u32' }), // expiry_ledger
139+
Address.fromString(params.fundingSource).toScVal(), // recovery_address
140+
),
141+
)
142+
.setTimeout(30)
143+
.build();
144+
145+
// Simulate the transaction first to check for errors
146+
const simulated = await this.sorobanServer.simulateTransaction(transaction);
147+
148+
if (rpc.Api.isSimulationError(simulated)) {
149+
throw new Error(`Contract initialization failed: ${simulated.error}`);
150+
}
151+
152+
// Prepare and sign the transaction
153+
const preparedTx = rpc
154+
.assembleTransaction(transaction, simulated)
155+
.build();
156+
preparedTx.sign(ephemeralKeypair);
157+
158+
// Submit to the network
159+
const contractResult = await this.sorobanServer.sendTransaction(preparedTx);
160+
161+
if (contractResult.status === 'ERROR') {
162+
throw new Error(
163+
`Contract initialization failed on-chain: ${JSON.stringify(contractResult.errorResult)}`,
164+
);
165+
}
166+
167+
this.logger.log(
168+
`Contract initialized successfully for: ${params.ephemeralPublicKey}`,
169+
);
170+
}
63171
private getNetworkPassphrase(): string {
64172
return this.network === 'mainnet'
65173
? StellarSdk.Networks.PUBLIC

0 commit comments

Comments
 (0)