Solana program to send multiple lamport transfers and emit event

1.7k Views Asked by At

I’m building a program intended to manage multiple payments with one call. The program needs to complete the following steps:

  • Accept a certain amount of lamports
  • Pay a portion of the received lamports to specified wallets, such that the amount received is exhausted
  • Emit an event containing the receipt

I’ve built this logic with an Ethereum smart contract and it works perfectly fine, however when attempting to write a Solana program with Solang and @solana/solidity, I’m running into a number of issues.

The first issue I encountered was simply that @solana/solidity seems to not be built for front end use (transactions required a private key as an argument, rather than being signed by a wallet like Phantom) so I built a fork of the repository that exposes the transaction object to be signed. I also found that the signer’s key needed to be manually added to the array of keys in the transaction instruction — see this Stack Overflow post for more information, including the front end code used to sign and send the transaction.

However, after this post I ran into more errors, take the following for example:

Transaction simulation failed: Attempt to debit an account but found no record of a prior credit.
Transaction simulation failed: Error processing Instruction 0: instruction changed the balance of a read-only account 
    Program jdN1wZjg5P4xi718DG2HraGuxVx1mM7ebjXpxbJ5R3N invoke [1]
    Program data: PO+eZwYByRZpDC4BOjWoKPj20gquFc/JtyxU9NsuG/Y= DEjYtM7vwjNW3HPewJU3dvG4aiov5tUUlrD6Zz5ylBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADppnQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATEtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVNC02S0gyMV9Sa3RZZVJIb3FKOFpFAAAAAAAAAAAAAAA=
    Program jdN1wZjg5P4xi718DG2HraGuxVx1mM7ebjXpxbJ5R3N consumed 3850 of 200000 compute units
    Program jdN1wZjg5P4xi718DG2HraGuxVx1mM7ebjXpxbJ5R3N success
    failed to verify account 11111111111111111111111111111111: instruction changed the balance of a read-only account

The error messages seemed to be inconsistent, with some attempts throwing different errors despite the only changes in the code being a server restarting or a library being reinstalled.

Although solutions to the previous errors would be greatly appreciated, at this point I’m more inclined to ask more broadly if what I’m trying to do is possible, and, providing the source code, for help understanding what I need to do to make it work.

Below is the working source code for my Ethereum contract:

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

contract MyContract {
    event Receipt(
        address From,
                address Token,
        address[] Receivers,
        uint256[] Amounts,
        string Payment
    );

    function send( 
        address[] calldata _receivers,
        uint256[] calldata _amounts,
        string calldata _payment
    ) external payable {
        require(
            _receivers.length == _amounts.length,
            "Receiver count does not match amount count."
        );

        uint256 total;
        for (uint8 i; i < _receivers.length; i++) {
            total += _amounts[i];
        }
        require(
            total == msg.value,
            "Total payment value does not match ether sent"
        );

        for (uint8 i; i < _receivers.length; i++) {
            (bool sent, ) = _receivers[i].call{value: _amounts[i]}("");
            require(sent, "Transfer failed.");
        }

        emit Receipt(
            msg.sender,
            0x0000000000000000000000000000000000000000,
            _receivers,
            _amounts,
            _payment
        );
    }
}

The only differences between this code and my Solana program code are types and the method used to transfer lamports. All references to uint256 is replaced by uint64, the placeholder token address is changed from the null address to the system public key (address"11111111111111111111111111111111"), and the payment loop is changed to the following:

for (uint8 i = 0; i < _receivers.length; i++) {
    payable(_receivers[i]).transfer(_amounts[i]); // Using .send() throws the same error
}

The code used to then deploy the program to the Solana test validator is as follows, only slightly modified from the example provided by @solana/solidity:

const { Connection, LAMPORTS_PER_SOL, Keypair, PublicKey } = require('@solana/web3.js');
const { Contract } = require('@solana/solidity');
const { readFileSync } = require('fs');

const PROGRAM_ABI = JSON.parse(readFileSync('./build/sol/MyProgram.abi', 'utf8'));
const BUNDLE_SO = readFileSync('./build/sol/bundle.so');

(async function () {
  console.log('Connecting to your local Solana node');
  const connection = new Connection('http://localhost:8899', 'confirmed');

  const payer = Keypair.generate();

  async function airdrop(pubkey, amnt) {
    const sig = await connection.requestAirdrop(pubkey, amnt * LAMPORTS_PER_SOL);
    return connection.confirmTransaction(sig);
  }

  console.log('Airdropping SOL to a new wallet');
  await airdrop(payer.publicKey, 100);

  const program = new Keypair({
    publicKey: new Uint8Array([...]),
    secretKey: new Uint8Array([...])
  });

  const storage = new Keypair({
    publicKey: new Uint8Array([...]),
    secretKey: new Uint8Array([...])
  });

  const contract = new Contract(connection, program.publicKey, storage.publicKey, PROGRAM_ABI, payer);

  console.log('Loading the program');
  await contract.load(program, BUNDLE_SO);

  console.log('Deploying the program');
  await contract.deploy('MyProgram', [], program, storage, 4096 * 8);

  console.log('Program deployed!');

  process.exit(0);
})();

Is there something I’m misunderstanding or misusing here? I find it hard to believe that such simple behavior on the Ethereum blockchain couldn’t be replicated on Solana — especially given the great lengths the community has gone to to make Solana programming accessible through Solidity. If there’s something I’m doing wrong with this code I’d love to learn. Thank you so much in advance.

Edit: After upgrading my solang version, the first error was fixed. However, I'm now getting another error:

Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: instruction changed the balance of a read-only account

I'm not sure which account is supposedly read-only, as it isn't listed in the error response, but I'm pretty sure the only read-only account involved is the program as it's executable. How can I avoid this error?

1

There are 1 best solutions below

3
On

The error Attempt to debit an account but found no record of a prior credit happens when you attempt to airdrop more than 1 SOL. If you wish to have more than 1 SOL, then airdrop 1 SOL in a loop until you have enough.