mirror of
https://github.com/luxfi/captable.git
synced 2026-10-30 17:01:02 +00:00
619 lines
18 KiB
Go
619 lines
18 KiB
Go
// EFD adapter — NASAA Electronic Filing Depository.
|
|
//
|
|
// Implements notice filings (initial, renewal, amendment) and status
|
|
// queries against the NASAA EFD API. EFD accepts Form D notice
|
|
// filings for 49 states + DC (Florida and Texas are excluded — those
|
|
// states require state-portal submission, handled by separate
|
|
// adapters in this package).
|
|
//
|
|
// EFD payment integration uses ACH for filing fees + system-use fees.
|
|
// The system-use fee is currently $160 per filing; state fees vary
|
|
// per state-statute and are returned by CalculateFee.
|
|
//
|
|
// Source-of-design: Public-Spec
|
|
// Source-ref: http://nasaaefd.org
|
|
// Source-ref: http://nasaaefd.org/About/FormDStates
|
|
|
|
package bluesky
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// EFD environment URLs.
|
|
const (
|
|
// EFDProdURL is the production EFD API endpoint.
|
|
EFDProdURL = "https://www.efdnasaa.org"
|
|
|
|
// EFDTestURL is the EFD test/sandbox endpoint.
|
|
EFDTestURL = "https://test.efdnasaa.org"
|
|
)
|
|
|
|
// EFDSystemFeeUSD is the per-filing NASAA system-use fee charged on
|
|
// every EFD submission, layered on top of the state fee.
|
|
const EFDSystemFeeUSD = 160.00
|
|
|
|
// EFDConfig holds the EFD-specific configuration.
|
|
type EFDConfig struct {
|
|
// BaseURL is one of EFDProdURL or EFDTestURL. Required.
|
|
BaseURL string
|
|
|
|
// Username is the EFD account username.
|
|
Username string
|
|
|
|
// Password is the EFD account password. Loaded from KMS; never
|
|
// logged.
|
|
Password string
|
|
|
|
// FirmCRD is the SEC/FINRA CRD number for the filing firm (broker-
|
|
// dealer of record). Required for filings made by a registered
|
|
// broker-dealer; optional for issuer-direct filings.
|
|
FirmCRD string
|
|
|
|
// PayerName / PayerAccount are the ACH-payer details for system-
|
|
// use + state fees, registered in EFD account settings.
|
|
PayerName string
|
|
PayerAccount string
|
|
}
|
|
|
|
// EFDAdapter is the NASAA EFD submission client. Wrapped per-state by
|
|
// efdStateWrapper so each state's adapter reports its own State()
|
|
// while sharing the underlying transport.
|
|
type EFDAdapter struct {
|
|
cfg EFDConfig
|
|
client *http.Client
|
|
userAgent string
|
|
maxRetries int
|
|
retryBaseDelay time.Duration
|
|
retryMaxDelay time.Duration
|
|
rand *rand.Rand
|
|
token string
|
|
tokenExpires time.Time
|
|
}
|
|
|
|
// NewEFDAdapter constructs an EFD adapter.
|
|
func NewEFDAdapter(cfg EFDConfig, client *http.Client, ua string, maxRetries int, baseDelay, maxDelay time.Duration) *EFDAdapter {
|
|
if cfg.BaseURL == "" {
|
|
cfg.BaseURL = EFDProdURL
|
|
}
|
|
return &EFDAdapter{
|
|
cfg: cfg,
|
|
client: client,
|
|
userAgent: ua,
|
|
maxRetries: maxRetries,
|
|
retryBaseDelay: baseDelay,
|
|
retryMaxDelay: maxDelay,
|
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
|
|
}
|
|
}
|
|
|
|
// efdStateWrapper makes the shared EFDAdapter implement StateAdapter
|
|
// for a specific state.
|
|
type efdStateWrapper struct {
|
|
efd *EFDAdapter
|
|
state State
|
|
}
|
|
|
|
func (w *efdStateWrapper) State() State { return w.state }
|
|
func (w *efdStateWrapper) SupportsElectronic() bool { return true }
|
|
|
|
func (w *efdStateWrapper) FileNoticeOfSale(ctx context.Context, n *NoticeFiling) (*Acknowledgment, error) {
|
|
return w.efd.fileNoticeOfSale(ctx, w.state, n)
|
|
}
|
|
func (w *efdStateWrapper) RenewNotice(ctx context.Context, r *RenewalFiling) (*Acknowledgment, error) {
|
|
return w.efd.renewNotice(ctx, w.state, r)
|
|
}
|
|
func (w *efdStateWrapper) GetFilingStatus(ctx context.Context, filingID string) (*FilingStatus, error) {
|
|
return w.efd.getFilingStatus(ctx, w.state, filingID)
|
|
}
|
|
func (w *efdStateWrapper) CalculateFee(n *NoticeFiling) (FeeAmount, error) {
|
|
return w.efd.calculateFee(w.state, n)
|
|
}
|
|
|
|
// --- EFD HTTP transport ---
|
|
|
|
// efdLoginRequest is the body for POST /Account/Login.
|
|
type efdLoginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
FirmCRD string `json:"firmCRD,omitempty"`
|
|
}
|
|
|
|
// efdLoginResponse is the reply from POST /Account/Login.
|
|
type efdLoginResponse struct {
|
|
Token string `json:"token"`
|
|
Expires string `json:"expires"`
|
|
}
|
|
|
|
// efdFilingRequest is the body for POST /FormD/Submit.
|
|
type efdFilingRequest struct {
|
|
State string `json:"state"`
|
|
FilingType string `json:"filingType"`
|
|
Issuer efdIssuerPayload `json:"issuer"`
|
|
Offering efdOfferingPayload `json:"offering"`
|
|
Signature efdSignaturePayload `json:"signature"`
|
|
SECFileNumber string `json:"secFileNumber,omitempty"`
|
|
SECAccessionNumber string `json:"secAccessionNumber,omitempty"`
|
|
FormDXML string `json:"formDXMLBase64,omitempty"` // base64 attached if provided
|
|
}
|
|
|
|
type efdIssuerPayload struct {
|
|
CIK string `json:"cik,omitempty"`
|
|
EntityName string `json:"entityName"`
|
|
EntityType string `json:"entityType"`
|
|
Jurisdiction string `json:"jurisdiction"`
|
|
YearOfInc string `json:"yearOfIncorporation,omitempty"`
|
|
Street1 string `json:"street1"`
|
|
Street2 string `json:"street2,omitempty"`
|
|
City string `json:"city"`
|
|
State string `json:"state"`
|
|
PostalCode string `json:"postalCode"`
|
|
Country string `json:"country"`
|
|
Phone string `json:"phone"`
|
|
Email string `json:"email,omitempty"`
|
|
}
|
|
|
|
type efdOfferingPayload struct {
|
|
FederalExemption string `json:"federalExemption"`
|
|
TotalOfferingAmount float64 `json:"totalOfferingAmount"`
|
|
AmountSoldInState float64 `json:"amountSoldInState"`
|
|
DateOfFirstSaleInState string `json:"dateOfFirstSaleInState"` // YYYY-MM-DD
|
|
TypesOfSecurities []string `json:"typesOfSecurities"`
|
|
}
|
|
|
|
type efdSignaturePayload struct {
|
|
IssuerName string `json:"issuerName"`
|
|
SignatureName string `json:"signatureName"`
|
|
NameOfSigner string `json:"nameOfSigner"`
|
|
SignatureTitle string `json:"signatureTitle"`
|
|
SignatureDate string `json:"signatureDate"`
|
|
}
|
|
|
|
// efdFilingResponse is the reply from POST /FormD/Submit.
|
|
type efdFilingResponse struct {
|
|
FilingID string `json:"filingId"`
|
|
Status string `json:"status"`
|
|
State string `json:"state"`
|
|
Fee float64 `json:"fee"`
|
|
ReceivedAt string `json:"receivedAt"`
|
|
ExpiresAt string `json:"expiresAt"`
|
|
PaymentID string `json:"paymentId"`
|
|
Messages []string `json:"messages"`
|
|
}
|
|
|
|
// efdRenewalRequest is the body for POST /FormD/Renew.
|
|
type efdRenewalRequest struct {
|
|
State string `json:"state"`
|
|
OriginalFilingID string `json:"originalFilingId"`
|
|
Issuer efdIssuerPayload `json:"issuer"`
|
|
Offering efdOfferingPayload `json:"offering"`
|
|
Signature efdSignaturePayload `json:"signature"`
|
|
}
|
|
|
|
// efdStatusResponse is the reply from GET /FormD/Status/{filingId}.
|
|
type efdStatusResponse struct {
|
|
FilingID string `json:"filingId"`
|
|
State string `json:"state"`
|
|
Status string `json:"status"`
|
|
FiledAt string `json:"filedAt"`
|
|
AcceptedAt string `json:"acceptedAt"`
|
|
ExpiresAt string `json:"expiresAt"`
|
|
Fee float64 `json:"fee"`
|
|
Messages []string `json:"messages"`
|
|
}
|
|
|
|
// authenticate logs the adapter in to EFD if no fresh token is held.
|
|
func (a *EFDAdapter) authenticate(ctx context.Context) error {
|
|
if a.token != "" && time.Now().Before(a.tokenExpires.Add(-1*time.Minute)) {
|
|
return nil
|
|
}
|
|
if a.cfg.Username == "" || a.cfg.Password == "" {
|
|
return errors.New("bluesky: EFD credentials missing")
|
|
}
|
|
body, err := a.doJSON(ctx, http.MethodPost, "/Account/Login", efdLoginRequest{
|
|
Username: a.cfg.Username, Password: a.cfg.Password, FirmCRD: a.cfg.FirmCRD,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("bluesky: EFD login: %w", err)
|
|
}
|
|
var lr efdLoginResponse
|
|
if err := json.Unmarshal(body, &lr); err != nil {
|
|
return fmt.Errorf("bluesky: decode EFD login: %w", err)
|
|
}
|
|
a.token = lr.Token
|
|
if lr.Expires != "" {
|
|
if t, err := time.Parse(time.RFC3339, lr.Expires); err == nil {
|
|
a.tokenExpires = t
|
|
} else {
|
|
a.tokenExpires = time.Now().Add(15 * time.Minute)
|
|
}
|
|
} else {
|
|
a.tokenExpires = time.Now().Add(15 * time.Minute)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *EFDAdapter) fileNoticeOfSale(ctx context.Context, state State, n *NoticeFiling) (*Acknowledgment, error) {
|
|
if err := a.authenticate(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
req := buildEFDFilingRequest(state, n)
|
|
body, err := a.doJSON(ctx, http.MethodPost, "/FormD/Submit", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var resp efdFilingResponse
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, fmt.Errorf("bluesky: decode EFD submit: %w", err)
|
|
}
|
|
return efdAck(state, &resp), nil
|
|
}
|
|
|
|
func (a *EFDAdapter) renewNotice(ctx context.Context, state State, r *RenewalFiling) (*Acknowledgment, error) {
|
|
if err := a.authenticate(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
req := efdRenewalRequest{
|
|
State: string(state),
|
|
OriginalFilingID: r.OriginalFilingID,
|
|
Issuer: buildEFDIssuer(r.Issuer),
|
|
Offering: buildEFDOffering(r.UpdatedOffering),
|
|
Signature: buildEFDSignature(r.Signature),
|
|
}
|
|
body, err := a.doJSON(ctx, http.MethodPost, "/FormD/Renew", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var resp efdFilingResponse
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, fmt.Errorf("bluesky: decode EFD renew: %w", err)
|
|
}
|
|
return efdAck(state, &resp), nil
|
|
}
|
|
|
|
func (a *EFDAdapter) getFilingStatus(ctx context.Context, state State, filingID string) (*FilingStatus, error) {
|
|
if err := a.authenticate(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := a.doGet(ctx, "/FormD/Status/"+filingID)
|
|
if err != nil {
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
|
return nil, ErrFilingNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
var resp efdStatusResponse
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, fmt.Errorf("bluesky: decode EFD status: %w", err)
|
|
}
|
|
out := &FilingStatus{
|
|
FilingID: resp.FilingID,
|
|
State: state,
|
|
Status: strings.ToUpper(resp.Status),
|
|
Fee: resp.Fee,
|
|
Messages: resp.Messages,
|
|
}
|
|
if t, err := time.Parse(time.RFC3339, resp.FiledAt); err == nil {
|
|
out.FiledAt = t
|
|
}
|
|
if t, err := time.Parse(time.RFC3339, resp.AcceptedAt); err == nil {
|
|
out.AcceptedAt = t
|
|
}
|
|
if t, err := time.Parse(time.RFC3339, resp.ExpiresAt); err == nil {
|
|
out.ExpiresAt = t
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (a *EFDAdapter) calculateFee(state State, n *NoticeFiling) (FeeAmount, error) {
|
|
fee := stateFeeSchedule(state, n)
|
|
return FeeAmount{
|
|
State: state,
|
|
StateFee: fee.state,
|
|
SystemFee: EFDSystemFeeUSD,
|
|
TotalDue: fee.state + EFDSystemFeeUSD,
|
|
Currency: "USD",
|
|
Method: "ACH",
|
|
Notes: fee.notes,
|
|
}, nil
|
|
}
|
|
|
|
// --- HTTP helpers ---
|
|
|
|
// APIError is the structured error returned when EFD or a state
|
|
// portal replies with a non-2xx status.
|
|
type APIError struct {
|
|
StatusCode int
|
|
Endpoint string
|
|
Body string
|
|
RetryAfter time.Duration
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("bluesky API %d on %s: %s", e.StatusCode, e.Endpoint, e.Body)
|
|
}
|
|
|
|
// ErrRateLimited is returned when EFD or a state portal sustains
|
|
// 429 across the retry budget.
|
|
var ErrRateLimited = errors.New("bluesky: rate limited after retry budget exhausted")
|
|
|
|
func (a *EFDAdapter) doJSON(ctx context.Context, method, path string, body any) ([]byte, error) {
|
|
var reqBody []byte
|
|
if body != nil {
|
|
var err error
|
|
reqBody, err = json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bluesky: marshal: %w", err)
|
|
}
|
|
}
|
|
doOnce := func() (*http.Response, []byte, error) {
|
|
var reader io.Reader
|
|
if reqBody != nil {
|
|
reader = bytes.NewReader(reqBody)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, a.cfg.BaseURL+path, reader)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if reqBody != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", a.userAgent)
|
|
if a.token != "" && path != "/Account/Login" {
|
|
req.Header.Set("Authorization", "Bearer "+a.token)
|
|
}
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
rb, err := io.ReadAll(resp.Body)
|
|
return resp, rb, err
|
|
}
|
|
return a.doWithRetry(ctx, path, doOnce)
|
|
}
|
|
|
|
func (a *EFDAdapter) doGet(ctx context.Context, path string) ([]byte, error) {
|
|
doOnce := func() (*http.Response, []byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.cfg.BaseURL+path, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", a.userAgent)
|
|
if a.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+a.token)
|
|
}
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
return resp, body, err
|
|
}
|
|
return a.doWithRetry(ctx, path, doOnce)
|
|
}
|
|
|
|
func (a *EFDAdapter) doWithRetry(ctx context.Context, path string, fn func() (*http.Response, []byte, error)) ([]byte, error) {
|
|
var lastBody []byte
|
|
var lastStatus int
|
|
var lastRetryAfter time.Duration
|
|
for attempt := 0; attempt <= a.maxRetries; attempt++ {
|
|
resp, body, err := fn()
|
|
if err != nil {
|
|
if attempt == a.maxRetries {
|
|
return nil, err
|
|
}
|
|
if waitErr := a.sleepBackoff(ctx, attempt, 0); waitErr != nil {
|
|
return nil, waitErr
|
|
}
|
|
continue
|
|
}
|
|
lastBody = body
|
|
lastStatus = resp.StatusCode
|
|
if resp.StatusCode == http.StatusTooManyRequests ||
|
|
(resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
|
|
lastRetryAfter = parseRetryAfter(resp.Header.Get("Retry-After"))
|
|
if attempt == a.maxRetries {
|
|
break
|
|
}
|
|
if waitErr := a.sleepBackoff(ctx, attempt, lastRetryAfter); waitErr != nil {
|
|
return nil, waitErr
|
|
}
|
|
continue
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
return nil, &APIError{
|
|
StatusCode: resp.StatusCode, Endpoint: path, Body: string(body),
|
|
}
|
|
}
|
|
return body, nil
|
|
}
|
|
apiErr := &APIError{
|
|
StatusCode: lastStatus, Endpoint: path, Body: string(lastBody), RetryAfter: lastRetryAfter,
|
|
}
|
|
if lastStatus == http.StatusTooManyRequests {
|
|
return nil, fmt.Errorf("%w: %s", ErrRateLimited, apiErr.Error())
|
|
}
|
|
return nil, apiErr
|
|
}
|
|
|
|
func (a *EFDAdapter) sleepBackoff(ctx context.Context, attempt int, retryAfter time.Duration) error {
|
|
var wait time.Duration
|
|
if retryAfter > 0 {
|
|
wait = retryAfter
|
|
} else {
|
|
exp := a.retryBaseDelay << attempt
|
|
if exp <= 0 || exp > a.retryMaxDelay {
|
|
exp = a.retryMaxDelay
|
|
}
|
|
wait = time.Duration(a.rand.Int63n(int64(exp) + 1))
|
|
}
|
|
t := time.NewTimer(wait)
|
|
defer t.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-t.C:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func parseRetryAfter(s string) time.Duration {
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
if secs, err := strconv.Atoi(strings.TrimSpace(s)); err == nil && secs >= 0 {
|
|
return time.Duration(secs) * time.Second
|
|
}
|
|
if t, err := http.ParseTime(s); err == nil {
|
|
d := time.Until(t)
|
|
if d > 0 {
|
|
return d
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// --- Builders ---
|
|
|
|
func buildEFDFilingRequest(state State, n *NoticeFiling) efdFilingRequest {
|
|
req := efdFilingRequest{
|
|
State: string(state),
|
|
FilingType: string(n.FilingType),
|
|
Issuer: buildEFDIssuer(n.Issuer),
|
|
Offering: buildEFDOffering(n.Offering),
|
|
Signature: buildEFDSignature(n.Signature),
|
|
SECFileNumber: n.Offering.SECFileNumber,
|
|
SECAccessionNumber: n.Offering.SECAccessionNumber,
|
|
}
|
|
if len(n.FormDXML) > 0 {
|
|
req.FormDXML = base64Std(n.FormDXML)
|
|
}
|
|
if req.FilingType == "" {
|
|
req.FilingType = string(FilingNoticeOfSale)
|
|
}
|
|
return req
|
|
}
|
|
|
|
func buildEFDIssuer(i Issuer) efdIssuerPayload {
|
|
return efdIssuerPayload{
|
|
CIK: i.CIK,
|
|
EntityName: i.EntityName,
|
|
EntityType: i.EntityType,
|
|
Jurisdiction: i.JurisdictionOfIncorporation,
|
|
YearOfInc: i.YearOfIncorporation,
|
|
Street1: i.PrimaryAddress.Street1,
|
|
Street2: i.PrimaryAddress.Street2,
|
|
City: i.PrimaryAddress.City,
|
|
State: i.PrimaryAddress.State,
|
|
PostalCode: i.PrimaryAddress.PostalCode,
|
|
Country: i.PrimaryAddress.Country,
|
|
Phone: i.Phone,
|
|
Email: i.Email,
|
|
}
|
|
}
|
|
|
|
func buildEFDOffering(o OfferingDetails) efdOfferingPayload {
|
|
return efdOfferingPayload{
|
|
FederalExemption: o.FederalExemption,
|
|
TotalOfferingAmount: o.TotalOfferingAmount,
|
|
AmountSoldInState: o.AmountSoldInState,
|
|
DateOfFirstSaleInState: o.DateOfFirstSaleInState.Format("2006-01-02"),
|
|
TypesOfSecurities: o.TypesOfSecurities,
|
|
}
|
|
}
|
|
|
|
func buildEFDSignature(s Signature) efdSignaturePayload {
|
|
return efdSignaturePayload{
|
|
IssuerName: s.IssuerName,
|
|
SignatureName: s.SignatureName,
|
|
NameOfSigner: s.NameOfSigner,
|
|
SignatureTitle: s.SignatureTitle,
|
|
SignatureDate: s.SignatureDate,
|
|
}
|
|
}
|
|
|
|
func efdAck(state State, resp *efdFilingResponse) *Acknowledgment {
|
|
out := &Acknowledgment{
|
|
FilingID: resp.FilingID,
|
|
Status: strings.ToUpper(resp.Status),
|
|
State: state,
|
|
Fee: resp.Fee,
|
|
PaymentID: resp.PaymentID,
|
|
Messages: resp.Messages,
|
|
}
|
|
if t, err := time.Parse(time.RFC3339, resp.ReceivedAt); err == nil {
|
|
out.ReceivedAt = t
|
|
}
|
|
if t, err := time.Parse(time.RFC3339, resp.ExpiresAt); err == nil {
|
|
out.ExpiresAt = t
|
|
}
|
|
if out.Status == "" {
|
|
out.Status = "RECEIVED"
|
|
}
|
|
return out
|
|
}
|
|
|
|
// efdStates returns the canonical list of states that accept EFD
|
|
// notice filings per NASAA's published participation list. All 50
|
|
// states + DC except FL and TX.
|
|
func efdStates() []State {
|
|
return []State{
|
|
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE",
|
|
"GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY",
|
|
"LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO",
|
|
"MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC",
|
|
"ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD",
|
|
"TN", "UT", "VT", "VA", "WA", "WV", "WI", "WY",
|
|
"DC",
|
|
}
|
|
}
|
|
|
|
// base64Std encodes bytes as standard (padded) base64.
|
|
func base64Std(b []byte) string {
|
|
const tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
if len(b) == 0 {
|
|
return ""
|
|
}
|
|
out := make([]byte, ((len(b)+2)/3)*4)
|
|
j := 0
|
|
for i := 0; i < len(b); i += 3 {
|
|
var n uint32
|
|
n |= uint32(b[i]) << 16
|
|
if i+1 < len(b) {
|
|
n |= uint32(b[i+1]) << 8
|
|
}
|
|
if i+2 < len(b) {
|
|
n |= uint32(b[i+2])
|
|
}
|
|
out[j+0] = tbl[(n>>18)&0x3F]
|
|
out[j+1] = tbl[(n>>12)&0x3F]
|
|
if i+1 < len(b) {
|
|
out[j+2] = tbl[(n>>6)&0x3F]
|
|
} else {
|
|
out[j+2] = '='
|
|
}
|
|
if i+2 < len(b) {
|
|
out[j+3] = tbl[n&0x3F]
|
|
} else {
|
|
out[j+3] = '='
|
|
}
|
|
j += 4
|
|
}
|
|
return string(out)
|
|
}
|