0

I'm trying to setup a callout class to properly handle the passing in of a few variables to build an endpoint string. As you can see below I'm struggling with how and where to declare certain variables. With the below code, anywhere else the variables are referenced (methods) I get an error saying the variables don't exist. I'm not sure what the best practice is here with everything you see below. What should be managed in a constructor? What doesn't need to be managed in a constructor? Is a constructor necessary at all? What should be static? Final? Errors out at request.setEndpoint(accountTrans) Errors out at opp.AccountId = accountId;

Both state 'Variable does not exist'

I know this is an issue of initialization. As the endpoint string that I have the variable accountAddress in returns null.

public class EtherscanRESTCallouts {

public String accountAddress;
public Id accountId;
public String accountTrans;

private static final API_Token__mdt API_RECORD = [SELECT API_Token__c, MasterLabel FROM API_Token__mdt WHERE MasterLabel = 'Etherscan' WITH SECURITY_ENFORCED];

private static final String SINGLE_ACCOUNT_BALANCE = 'https://api.etherscan.io/api?module=account&action=balance&' + 
                                                    'address=0x267f70f9b856de226fea5fc1b0a8e319c72ceff5&tag=latest&apikey=' + API_RECORD.API_Token__c;
private static final String MULTI_ACCOUNT_BALANCE = 'https://api.etherscan.io/api?module=account&action=balancemulti&' + 
                                                    'address=0x267f70f9b856de226fea5fc1b0a8e319c72ceff5,0x1b3cb81e51011b549d78bf720b0d924ac763a7c2,0x73bceb1cd57c711feac4224d062b0f6ff338501e&tag=latest&apikey=' + API_RECORD.API_Token__c;                                                    

public EtherscanRESTCallouts(String accountName, Id accId) {

    accountAddress = accountName;
    accountId = accId;

    accountTrans = 'https://api.etherscan.io/api?module=account&action=txlist&' + 
                            'address=' + accountAddress + '&startblock=0&endblock=99999999&sort=asc&apikey=' + API_RECORD.API_Token__c;
}

private static Decimal toEth(String wei) {
    Decimal ethAmount = Decimal.valueOf(wei).divide(10^18, 2, System.RoundingMode.UP);

    return ethAmount;
}

@future(callout=true)
public static void accountTransactions() {
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(accountTrans);
    request.setMethod('GET');
    request.setHeader('Content-Type', 'application/json;charset=UTF-8');
    HttpResponse response = http.send(request);
    // Parse the JSON response
    if (response.getStatusCode() == 200) {

        List<Opportunity> opportunitiesToInsert = new List<Opportunity>();

        JSONParser parser = JSON.createParser(response.getBody());
        EtherscanJSONHelper ethInstance = new EtherscanJSONHelper(parser);

        for(EtherscanJSONHelper.Result result : ethInstance.result) {
            Opportunity opp = new Opportunity();

            opp.Block_Number__c = Integer.valueOf(result.blockHash);
            opp.Time_Stamp__c = Datetime.newInstance(Long.valueOf(result.timeStamp));
            opp.Hash__c = result.hash;
            opp.Nonce__c = Integer.valueOf(result.nonce);
            opp.Block_Hash__c = result.blockHash;
            opp.Transaction_Index__c = Integer.valueOf(result.transactionIndex);
            opp.From_Address__c = result.from_Z;
            opp.To_Address__c = result.to;
            opp.Transaction_Amount__c = toEth(result.value);
            opp.Gas_Price_in_Eth__c = toEth(result.gasPrice);
            opp.isError__c = Boolean.valueOf(result.isError);
            opp.Transaction_Rec__c = Boolean.valueOf(result.txreceipt_status);
            opp.Input__c = result.input;
            opp.Contract_Address__c = result.contractAddress;
            opp.Cumulative_Gas_Used__c = Integer.valueOf(result.cumulativeGasUsed);
            opp.Gas_Used__c = Integer.valueOf(result.gasUsed);
            opp.Confirmations__c = Integer.valueOf(result.confirmations);
            opp.AccountId = accountId;

            opportunitiesToInsert.add(opp);
        }

        if(!opportunitiesToInsert.isEmpty()) {
            try {
                insert opportunitiesToInsert;
            } catch (DMLException e) {
                ApexPages.addMessages(e);

                Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                String[] toAddresses = new String[] {'djrdev@icloud.com'};
                mail.setToAddresses(toAddresses);
                mail.setReplyTo('djrdev@icloud.com');
                mail.setSenderDisplayName('Apex error message');
                mail.setSubject('Error from Org&nbsp;: ' + UserInfo.getOrganizationName());
                mail.setPlainTextBody(e.getMessage());
                Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
            }
        }

    } else {
        System.debug(response.getBody());
    }
}
  • 3
    Is this block of code wrapped in a class declaration? – David Reed Jun 11 at 4:44
  • Yes. Being called from a trigger handler. Sorry for how vague this is just tying to understand best practice. – djr Jun 11 at 14:22
  • 3
    I think we need to see a complete, self-contained, reproducible example. Right now you are not declaring any variables in your constructor, and without the complete context, it's hard to see if there are any errors related to your member variable.s – David Reed Jun 11 at 14:38
2

There are a number of issues here leading to the errors you're seeing.

Your constructor is not called when accountTransactions() is because accountTransactions is both a static function and a future method - meaning accountAddress, accountId, and accountTrans are not visible within the context of the method as they're properties that belong to an instance of EtherscanRESTCallouts. If anything so far isn't making sense, I would encourage you to read up on "classes vs objects" or do the Trailhead Salesforce has on the topic.

I think some refactoring needs to be done here in order to get what you want. There are multiple approaches.

  1. You may eliminate the future annotation, which you only need if you need to reset your transaction limits when the callout is made (for more CPU time, to reset DML limits, or prevent the "you have uncommitted work pending" error).

  2. You may eliminate the future annotation and make the method non-static. This has the benefit of giving your method visibility to non-static properties and the general flow of using the class then becomes:

    EtherscanRESTCallouts caller = new EtherscanRESTCallouts(arg1, arg2);
    caller.accountTransactions();
    
  3. Finally, you may keep the structure as it is, but you will need to pass arguments to the future method so that it has context for everything that it needs to execute its logic. (Documentation on future methods)

One small note, it's bad practice to have a query run when referencing a class, I would make API_RECORD lazy load, so that it only executes the query when its referenced and therefore needed.

| improve this answer | |
  • 1
    @djr, Taking a step back, it may be easiest to put together everything you want to do by breaking apart your code a little bit and just executing the HTTP callout imperatively in execute anonymous first, verifying that your results are correct, and then move it into your class. Often times eliminating "clutter" (the rest of the class in this case) can help you see the true problem. – nbrown Jun 11 at 16:10
  • I failed to mention this is being called from a trigger. Not sure if that changes anything about your response but I'm going to play around with the different ideas you have put forward here. – djr Jun 16 at 19:58
  • I remembered that a callout from a trigger has to be done async. So #3 it is. – djr Jun 16 at 20:27
  • @djr, yep you can do a future method or perhaps queue up a queueable. Just remember that in either scenario you're essentially starting with a clean slate so whatever gets passed to the future method or the queueable is all that will be available to it when it starts. Everything else is out of context because it's a new transaction. – nbrown Jun 16 at 21:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.